sticky-note-cli 3.1.40 → 3.1.42
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/bin/cli.js +13 -3
- package/package.json +1 -1
- package/templates/hooks/data-branch.js +77 -12
- package/templates/hooks/sticky-utils.js +9 -0
package/bin/cli.js
CHANGED
|
@@ -720,12 +720,22 @@ async function cmdInit() {
|
|
|
720
720
|
// Teammate flow: URL already in committed config — just need their API key
|
|
721
721
|
print(`\n Cloud backend detected: ${configStickyUrl}`);
|
|
722
722
|
const keyAnswer = await ask(rl, "Enter your STICKY_API_KEY (get it from your team lead)", "");
|
|
723
|
-
|
|
723
|
+
// GitHub push token — needed so session-end can push the data branch from
|
|
724
|
+
// background hook processes that don't have keychain/credential-manager access.
|
|
725
|
+
print("\n A GitHub Personal Access Token (PAT) is needed so sticky-note can push");
|
|
726
|
+
print(" thread data in the background without requiring interactive credential access.");
|
|
727
|
+
print(" Create one at https://github.com/settings/tokens with 'repo' scope (or");
|
|
728
|
+
print(" 'contents: write' for fine-grained tokens). Your team lead may share one.");
|
|
729
|
+
const pushTokenAnswer = await ask(rl, "Enter your STICKY_PUSH_TOKEN (GitHub PAT)", "");
|
|
730
|
+
if (keyAnswer.trim() || pushTokenAnswer.trim()) {
|
|
724
731
|
const envPath = path.join(process.cwd(), ".env.sticky");
|
|
725
|
-
|
|
732
|
+
let envContent = `STICKY_URL=${configStickyUrl}\n`;
|
|
733
|
+
if (keyAnswer.trim()) envContent += `STICKY_API_KEY=${keyAnswer.trim()}\n`;
|
|
734
|
+
if (pushTokenAnswer.trim()) envContent += `STICKY_PUSH_TOKEN=${pushTokenAnswer.trim()}\n`;
|
|
735
|
+
fs.writeFileSync(envPath, envContent, "utf-8");
|
|
726
736
|
print(" [OK] .env.sticky written");
|
|
727
737
|
} else {
|
|
728
|
-
print(" ⏭️ No
|
|
738
|
+
print(" ⏭️ No keys entered — skipping .env.sticky (local-only mode)");
|
|
729
739
|
}
|
|
730
740
|
} else {
|
|
731
741
|
const cloudAnswer = await ask(
|
package/package.json
CHANGED
|
@@ -19,6 +19,52 @@ const DATA_REF = "refs/heads/" + DATA_BRANCH;
|
|
|
19
19
|
|
|
20
20
|
const GIT_OPTS = { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] };
|
|
21
21
|
|
|
22
|
+
// ── Credential helpers ────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read STICKY_PUSH_TOKEN from env or .env.sticky file.
|
|
26
|
+
* Returns the token string or null.
|
|
27
|
+
*/
|
|
28
|
+
function _getPushToken() {
|
|
29
|
+
if (process.env.STICKY_PUSH_TOKEN) return process.env.STICKY_PUSH_TOKEN;
|
|
30
|
+
try {
|
|
31
|
+
// Walk up to find .env.sticky
|
|
32
|
+
let dir = process.cwd();
|
|
33
|
+
for (let i = 0; i < 20; i++) {
|
|
34
|
+
const envPath = path.join(dir, ".env.sticky");
|
|
35
|
+
if (fs.existsSync(envPath)) {
|
|
36
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
37
|
+
const match = content.match(/^STICKY_PUSH_TOKEN=(.+)$/m);
|
|
38
|
+
if (match) return match[1].trim();
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
const parent = path.dirname(dir);
|
|
42
|
+
if (parent === dir) break;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
} catch (_) {}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Given a remote name, return an authenticated push URL if STICKY_PUSH_TOKEN
|
|
51
|
+
* is configured. Embeds the token into the HTTPS URL so the push works from
|
|
52
|
+
* any process context without needing a credential helper.
|
|
53
|
+
* Returns null if no token or remote URL is not HTTPS.
|
|
54
|
+
*/
|
|
55
|
+
function _getAuthenticatedUrl(remote) {
|
|
56
|
+
const token = _getPushToken();
|
|
57
|
+
if (!token) return null;
|
|
58
|
+
try {
|
|
59
|
+
const url = execFileSync("git", ["remote", "get-url", remote], GIT_OPTS).trim();
|
|
60
|
+
if (!url.startsWith("https://")) return null;
|
|
61
|
+
// Insert token: https://TOKEN@github.com/...
|
|
62
|
+
return url.replace(/^https:\/\/([^@]*)@?/, `https://${token}@`);
|
|
63
|
+
} catch (_) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
22
68
|
// ── Git helpers ───────────────────────────────────────────
|
|
23
69
|
|
|
24
70
|
function getDefaultRemote() {
|
|
@@ -164,10 +210,13 @@ function pushDataBranch(remote, branchName, maxRetries, localMemPath, loadJsonFn
|
|
|
164
210
|
if (!remote) return { ok: false, error: "no remote configured" };
|
|
165
211
|
|
|
166
212
|
const pushSpec = DATA_REF + ":" + DATA_REF;
|
|
213
|
+
// Use token-authenticated URL if available — bypasses credential helpers so
|
|
214
|
+
// pushes work from background hook processes (no keychain/credential-manager needed).
|
|
215
|
+
const pushTarget = _getAuthenticatedUrl(remote) || remote;
|
|
167
216
|
|
|
168
217
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
169
218
|
try {
|
|
170
|
-
execFileSync("git", ["push",
|
|
219
|
+
execFileSync("git", ["push", pushTarget, pushSpec], {
|
|
171
220
|
timeout: 30000, stdio: ["pipe", "pipe", "pipe"],
|
|
172
221
|
});
|
|
173
222
|
return { ok: true, error: null };
|
|
@@ -188,22 +237,28 @@ function pushDataBranch(remote, branchName, maxRetries, localMemPath, loadJsonFn
|
|
|
188
237
|
).trim();
|
|
189
238
|
|
|
190
239
|
// Build merged file map: start with remote files as base, then
|
|
191
|
-
// overlay local files so this user's audit/presence are preserved
|
|
240
|
+
// overlay local files so this user's audit/presence are preserved.
|
|
241
|
+
// Capture remote sticky-note.json BEFORE the local overlay so we
|
|
242
|
+
// have the actual remote content to merge (not the local overwrite).
|
|
192
243
|
const fileMap = {};
|
|
193
244
|
const localRef = "refs/heads/" + branchName;
|
|
245
|
+
let remoteStickyContent = null;
|
|
194
246
|
for (const f of listFilesInBranch(fetchResult.remoteRef)) {
|
|
195
247
|
const content = readFileFromBranch(fetchResult.remoteRef, f);
|
|
196
|
-
if (content !== null)
|
|
248
|
+
if (content !== null) {
|
|
249
|
+
fileMap[f] = content;
|
|
250
|
+
if (f === "sticky-note.json") remoteStickyContent = content;
|
|
251
|
+
}
|
|
197
252
|
}
|
|
198
253
|
for (const f of listFilesInBranch(localRef)) {
|
|
199
254
|
const content = readFileFromBranch(localRef, f);
|
|
200
255
|
if (content !== null) fileMap[f] = content;
|
|
201
256
|
}
|
|
202
257
|
|
|
203
|
-
// Merge sticky-note.json from both sides
|
|
204
|
-
if (localMemPath &&
|
|
258
|
+
// Merge sticky-note.json from both sides using captured remote content
|
|
259
|
+
if (localMemPath && remoteStickyContent) {
|
|
205
260
|
mergeAndSaveFromRemote(
|
|
206
|
-
localMemPath,
|
|
261
|
+
localMemPath, remoteStickyContent, loadJsonFn, saveJsonFn
|
|
207
262
|
);
|
|
208
263
|
fileMap["sticky-note.json"] = fs.readFileSync(localMemPath, "utf-8");
|
|
209
264
|
}
|
|
@@ -214,9 +269,12 @@ function pushDataBranch(remote, branchName, maxRetries, localMemPath, loadJsonFn
|
|
|
214
269
|
}
|
|
215
270
|
} catch (_) {}
|
|
216
271
|
|
|
217
|
-
// Brief exponential backoff
|
|
218
|
-
|
|
219
|
-
|
|
272
|
+
// Brief exponential backoff — use Atomics.wait to block without CPU spin
|
|
273
|
+
try {
|
|
274
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.pow(2, attempt) * 300);
|
|
275
|
+
} catch (_) {
|
|
276
|
+
// Atomics.wait unavailable (e.g. SharedArrayBuffer disabled) — skip delay
|
|
277
|
+
}
|
|
220
278
|
}
|
|
221
279
|
}
|
|
222
280
|
return { ok: false, error: "max retries exceeded" };
|
|
@@ -264,9 +322,16 @@ function mergeAndSaveFromRemote(localMemPath, remoteContent, loadJsonFn, saveJso
|
|
|
264
322
|
return; // corrupt remote — skip
|
|
265
323
|
}
|
|
266
324
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
325
|
+
let localMemory;
|
|
326
|
+
if (loadJsonFn) {
|
|
327
|
+
localMemory = loadJsonFn(localMemPath, { ...EMPTY_MEMORY });
|
|
328
|
+
} else {
|
|
329
|
+
try {
|
|
330
|
+
localMemory = JSON.parse(fs.readFileSync(localMemPath, "utf-8"));
|
|
331
|
+
} catch (_) {
|
|
332
|
+
localMemory = { ...EMPTY_MEMORY };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
270
335
|
|
|
271
336
|
const localThreads = Array.isArray(localMemory.threads) ? localMemory.threads.filter(Boolean) : [];
|
|
272
337
|
const remoteThreads = Array.isArray(remoteMemory.threads) ? remoteMemory.threads.filter(Boolean) : [];
|
|
@@ -641,6 +641,14 @@ function appendAuditLineBoth(entry, cloud) {
|
|
|
641
641
|
// ── Environment helpers ───────────────────────────────────
|
|
642
642
|
|
|
643
643
|
function getUser() {
|
|
644
|
+
// Prefer git config user.name — consistent across machines regardless of OS login name.
|
|
645
|
+
// Fall back to OS env vars, then "unknown".
|
|
646
|
+
try {
|
|
647
|
+
const name = execFileSync("git", ["config", "user.name"], {
|
|
648
|
+
encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
649
|
+
}).trim();
|
|
650
|
+
if (name) return name;
|
|
651
|
+
} catch (_) {}
|
|
644
652
|
return process.env.USER || process.env.USERNAME || "unknown";
|
|
645
653
|
}
|
|
646
654
|
|
|
@@ -1272,6 +1280,7 @@ function getCloudConfig() {
|
|
|
1272
1280
|
return {
|
|
1273
1281
|
url: process.env.STICKY_URL || envFile.STICKY_URL || "",
|
|
1274
1282
|
apiKey: process.env.STICKY_API_KEY || envFile.STICKY_API_KEY || "",
|
|
1283
|
+
pushToken: process.env.STICKY_PUSH_TOKEN || envFile.STICKY_PUSH_TOKEN || "",
|
|
1275
1284
|
};
|
|
1276
1285
|
}
|
|
1277
1286
|
|