@bretwardjames/tw-bridge 0.5.0 → 0.6.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/cli.js +370 -61
- package/dist/hooks/on-modify.js +328 -19
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import fs5 from "fs";
|
|
5
|
+
import path6 from "path";
|
|
6
|
+
import os5 from "os";
|
|
7
7
|
|
|
8
8
|
// src/config.ts
|
|
9
9
|
import fs from "fs";
|
|
@@ -241,8 +241,8 @@ async function oauthLogin(clientId, clientSecret, rl) {
|
|
|
241
241
|
saveTokens(tokens);
|
|
242
242
|
return tokens;
|
|
243
243
|
}
|
|
244
|
-
async function asanaFetch(
|
|
245
|
-
const url = `${API_BASE}${
|
|
244
|
+
async function asanaFetch(path7, token, options = {}) {
|
|
245
|
+
const url = `${API_BASE}${path7}`;
|
|
246
246
|
const res = await fetch(url, {
|
|
247
247
|
...options,
|
|
248
248
|
headers: {
|
|
@@ -258,9 +258,9 @@ async function asanaFetch(path6, token, options = {}) {
|
|
|
258
258
|
const json = await res.json();
|
|
259
259
|
return json.data;
|
|
260
260
|
}
|
|
261
|
-
async function asanaFetchAll(
|
|
261
|
+
async function asanaFetchAll(path7, token) {
|
|
262
262
|
const results = [];
|
|
263
|
-
let nextPage =
|
|
263
|
+
let nextPage = path7;
|
|
264
264
|
while (nextPage) {
|
|
265
265
|
const url = `${API_BASE}${nextPage}`;
|
|
266
266
|
const res = await fetch(url, {
|
|
@@ -572,10 +572,319 @@ function mapAsanaPriority(fields) {
|
|
|
572
572
|
return void 0;
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
+
// src/adapters/strety.ts
|
|
576
|
+
import fs3 from "fs";
|
|
577
|
+
import path4 from "path";
|
|
578
|
+
import os3 from "os";
|
|
579
|
+
import { execSync as execSync2 } from "child_process";
|
|
580
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
581
|
+
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
582
|
+
var API_BASE2 = "https://2.strety.com/api/v1";
|
|
583
|
+
var OAUTH_AUTHORIZE2 = `${API_BASE2}/oauth/authorize`;
|
|
584
|
+
var OAUTH_TOKEN2 = `${API_BASE2}/oauth/token`;
|
|
585
|
+
var TOKEN_FILE2 = path4.join(os3.homedir(), ".config", "tw-bridge", "strety-tokens.json");
|
|
586
|
+
function loadTokens2() {
|
|
587
|
+
try {
|
|
588
|
+
return JSON.parse(fs3.readFileSync(TOKEN_FILE2, "utf-8"));
|
|
589
|
+
} catch {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function saveTokens2(tokens) {
|
|
594
|
+
fs3.mkdirSync(path4.dirname(TOKEN_FILE2), { recursive: true });
|
|
595
|
+
fs3.writeFileSync(TOKEN_FILE2, JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
596
|
+
}
|
|
597
|
+
async function refreshAccessToken2(tokens) {
|
|
598
|
+
const now = Date.now();
|
|
599
|
+
if (tokens.access_token && tokens.expires_at > now + 3e5) {
|
|
600
|
+
return tokens.access_token;
|
|
601
|
+
}
|
|
602
|
+
const res = await fetch(OAUTH_TOKEN2, {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
605
|
+
body: new URLSearchParams({
|
|
606
|
+
grant_type: "refresh_token",
|
|
607
|
+
refresh_token: tokens.refresh_token,
|
|
608
|
+
client_id: tokens.client_id,
|
|
609
|
+
client_secret: tokens.client_secret
|
|
610
|
+
})
|
|
611
|
+
});
|
|
612
|
+
if (!res.ok) {
|
|
613
|
+
const body = await res.text();
|
|
614
|
+
throw new Error(`Token refresh failed (${res.status}): ${body}`);
|
|
615
|
+
}
|
|
616
|
+
const data = await res.json();
|
|
617
|
+
tokens.access_token = data.access_token;
|
|
618
|
+
if (data.refresh_token) {
|
|
619
|
+
tokens.refresh_token = data.refresh_token;
|
|
620
|
+
}
|
|
621
|
+
tokens.expires_at = now + (data.expires_in ?? 7200) * 1e3;
|
|
622
|
+
saveTokens2(tokens);
|
|
623
|
+
return tokens.access_token;
|
|
624
|
+
}
|
|
625
|
+
var REDIRECT_URI = "https://localhost/callback";
|
|
626
|
+
async function oauthLogin2(clientId, clientSecret, rl) {
|
|
627
|
+
const authUrl = `${OAUTH_AUTHORIZE2}?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=${encodeURIComponent("read write")}`;
|
|
628
|
+
console.log(`
|
|
629
|
+
Opening browser for authorization...`);
|
|
630
|
+
console.log(` If it doesn't open, visit:
|
|
631
|
+
${authUrl}
|
|
632
|
+
`);
|
|
633
|
+
try {
|
|
634
|
+
execSync2(`xdg-open "${authUrl}" 2>/dev/null || open "${authUrl}" 2>/dev/null`, {
|
|
635
|
+
stdio: "ignore"
|
|
636
|
+
});
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
console.log(" After authorizing, the browser will redirect to a page that");
|
|
640
|
+
console.log(" won't load. Copy the full URL from the address bar and paste it here.\n");
|
|
641
|
+
const pasted = (await rl.question("Paste the callback URL: ")).trim();
|
|
642
|
+
let code;
|
|
643
|
+
try {
|
|
644
|
+
const url = new URL(pasted);
|
|
645
|
+
const error = url.searchParams.get("error");
|
|
646
|
+
if (error) {
|
|
647
|
+
throw new Error(`OAuth error: ${error}`);
|
|
648
|
+
}
|
|
649
|
+
const authCode = url.searchParams.get("code");
|
|
650
|
+
if (!authCode) {
|
|
651
|
+
throw new Error("No authorization code found in URL");
|
|
652
|
+
}
|
|
653
|
+
code = authCode;
|
|
654
|
+
} catch (err) {
|
|
655
|
+
if (err instanceof Error && err.message.startsWith("OAuth error:")) throw err;
|
|
656
|
+
if (err instanceof Error && err.message.startsWith("No authorization")) throw err;
|
|
657
|
+
throw new Error(`Could not parse callback URL: ${pasted}`);
|
|
658
|
+
}
|
|
659
|
+
const tokenRes = await fetch(OAUTH_TOKEN2, {
|
|
660
|
+
method: "POST",
|
|
661
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
662
|
+
body: new URLSearchParams({
|
|
663
|
+
grant_type: "authorization_code",
|
|
664
|
+
code,
|
|
665
|
+
redirect_uri: REDIRECT_URI,
|
|
666
|
+
client_id: clientId,
|
|
667
|
+
client_secret: clientSecret
|
|
668
|
+
})
|
|
669
|
+
});
|
|
670
|
+
if (!tokenRes.ok) {
|
|
671
|
+
const body = await tokenRes.text();
|
|
672
|
+
throw new Error(`Token exchange failed (${tokenRes.status}): ${body}`);
|
|
673
|
+
}
|
|
674
|
+
const data = await tokenRes.json();
|
|
675
|
+
const tokens = {
|
|
676
|
+
access_token: data.access_token,
|
|
677
|
+
refresh_token: data.refresh_token,
|
|
678
|
+
expires_at: Date.now() + (data.expires_in ?? 7200) * 1e3,
|
|
679
|
+
client_id: clientId,
|
|
680
|
+
client_secret: clientSecret
|
|
681
|
+
};
|
|
682
|
+
saveTokens2(tokens);
|
|
683
|
+
return tokens;
|
|
684
|
+
}
|
|
685
|
+
async function stretyFetch(urlOrPath, token, options = {}) {
|
|
686
|
+
const url = urlOrPath.startsWith("http") ? urlOrPath : `${API_BASE2}${urlOrPath}`;
|
|
687
|
+
const res = await fetch(url, {
|
|
688
|
+
...options,
|
|
689
|
+
headers: {
|
|
690
|
+
Authorization: `Bearer ${token}`,
|
|
691
|
+
Accept: "application/vnd.api+json",
|
|
692
|
+
"Content-Type": "application/vnd.api+json",
|
|
693
|
+
...options.headers
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
if (!res.ok) {
|
|
697
|
+
const body = await res.text();
|
|
698
|
+
throw new Error(`Strety API ${res.status}: ${body}`);
|
|
699
|
+
}
|
|
700
|
+
return res.json();
|
|
701
|
+
}
|
|
702
|
+
async function stretyFetchAll(path7, token) {
|
|
703
|
+
const results = [];
|
|
704
|
+
let url = `${API_BASE2}${path7}`;
|
|
705
|
+
while (url) {
|
|
706
|
+
const json = await stretyFetch(url, token);
|
|
707
|
+
results.push(...json.data);
|
|
708
|
+
url = json.links?.next ?? null;
|
|
709
|
+
}
|
|
710
|
+
return results;
|
|
711
|
+
}
|
|
712
|
+
async function pickOne2(rl, label, items) {
|
|
713
|
+
console.log(`
|
|
714
|
+
${label}:`);
|
|
715
|
+
for (let i = 0; i < items.length; i++) {
|
|
716
|
+
const name = items[i].attributes.name ?? items[i].attributes.title ?? items[i].id;
|
|
717
|
+
console.log(` ${i + 1}) ${name}`);
|
|
718
|
+
}
|
|
719
|
+
while (true) {
|
|
720
|
+
const answer = await rl.question(`Choose (1-${items.length}): `);
|
|
721
|
+
const idx = parseInt(answer, 10) - 1;
|
|
722
|
+
if (idx >= 0 && idx < items.length) {
|
|
723
|
+
const item = items[idx];
|
|
724
|
+
return { id: item.id, name: item.attributes.name ?? item.attributes.title ?? item.id };
|
|
725
|
+
}
|
|
726
|
+
console.log(" Invalid choice, try again.");
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
var StretyAdapter = class {
|
|
730
|
+
name = "strety";
|
|
731
|
+
config;
|
|
732
|
+
accessToken = null;
|
|
733
|
+
async resolveToken() {
|
|
734
|
+
const auth = this.config.auth;
|
|
735
|
+
if (auth === "oauth") {
|
|
736
|
+
const tokens = loadTokens2();
|
|
737
|
+
if (!tokens) throw new Error("No OAuth tokens found. Run `tw-bridge add` to authenticate.");
|
|
738
|
+
this.accessToken = await refreshAccessToken2(tokens);
|
|
739
|
+
return this.accessToken;
|
|
740
|
+
}
|
|
741
|
+
if (this.accessToken) return this.accessToken;
|
|
742
|
+
if (auth.startsWith("env:")) {
|
|
743
|
+
const envVar = auth.slice(4);
|
|
744
|
+
const val = process.env[envVar];
|
|
745
|
+
if (!val) throw new Error(`Environment variable ${envVar} not set`);
|
|
746
|
+
this.accessToken = val;
|
|
747
|
+
return val;
|
|
748
|
+
}
|
|
749
|
+
this.accessToken = auth;
|
|
750
|
+
return auth;
|
|
751
|
+
}
|
|
752
|
+
defaultConfig(_cwd) {
|
|
753
|
+
return {
|
|
754
|
+
auth: "env:STRETY_TOKEN",
|
|
755
|
+
assignee_id: "",
|
|
756
|
+
assignee_name: ""
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
async setup(_cwd) {
|
|
760
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
761
|
+
try {
|
|
762
|
+
console.log("\n\u2500\u2500 Strety Adapter Setup \u2500\u2500\n");
|
|
763
|
+
console.log("Authentication:");
|
|
764
|
+
console.log(" 1) OAuth (browser login)");
|
|
765
|
+
console.log(" 2) Environment variable");
|
|
766
|
+
const authChoice = (await rl.question("\nChoose (1-2): ")).trim();
|
|
767
|
+
let authConfig;
|
|
768
|
+
let token;
|
|
769
|
+
if (authChoice === "1") {
|
|
770
|
+
console.log(`
|
|
771
|
+
Register a Strety app at My Integrations > My Apps.`);
|
|
772
|
+
console.log(` Set the Redirect URI to: ${REDIRECT_URI}
|
|
773
|
+
`);
|
|
774
|
+
const clientId = (await rl.question("Strety App Client ID: ")).trim();
|
|
775
|
+
const clientSecret = (await rl.question("Strety App Client Secret: ")).trim();
|
|
776
|
+
if (!clientId || !clientSecret) {
|
|
777
|
+
console.error("\n Client ID and Secret are required.");
|
|
778
|
+
rl.close();
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
const tokens = await oauthLogin2(clientId, clientSecret, rl);
|
|
782
|
+
token = tokens.access_token;
|
|
783
|
+
authConfig = "oauth";
|
|
784
|
+
} else {
|
|
785
|
+
const envVar = await rl.question("Environment variable name [STRETY_TOKEN]: ");
|
|
786
|
+
const varName = envVar.trim() || "STRETY_TOKEN";
|
|
787
|
+
authConfig = `env:${varName}`;
|
|
788
|
+
token = process.env[varName] ?? "";
|
|
789
|
+
if (!token) {
|
|
790
|
+
console.error(`
|
|
791
|
+
Cannot complete setup without a valid token.`);
|
|
792
|
+
console.error(` Set ${varName} in your environment and try again.`);
|
|
793
|
+
rl.close();
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
console.log("\nVerifying...");
|
|
798
|
+
let people;
|
|
799
|
+
try {
|
|
800
|
+
people = await stretyFetchAll("/people?page[size]=20", token);
|
|
801
|
+
} catch (err) {
|
|
802
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
803
|
+
console.error(` Authentication failed: ${msg}`);
|
|
804
|
+
rl.close();
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
console.log(` Connected! Found ${people.length} people.`);
|
|
808
|
+
const person = await pickOne2(rl, "Which person are you? (for filtering your todos)", people);
|
|
809
|
+
rl.close();
|
|
810
|
+
console.log(`
|
|
811
|
+
Assignee: ${person.name}`);
|
|
812
|
+
console.log(" Syncing: Todos assigned to you");
|
|
813
|
+
return {
|
|
814
|
+
auth: authConfig,
|
|
815
|
+
assignee_id: person.id,
|
|
816
|
+
assignee_name: person.name
|
|
817
|
+
};
|
|
818
|
+
} catch (err) {
|
|
819
|
+
rl.close();
|
|
820
|
+
throw err;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async init(config) {
|
|
824
|
+
this.config = config;
|
|
825
|
+
if (!this.config.assignee_id) {
|
|
826
|
+
throw new Error('strety adapter requires "assignee_id" in config');
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
async pull() {
|
|
830
|
+
const token = await this.resolveToken();
|
|
831
|
+
const raw = await stretyFetchAll(
|
|
832
|
+
`/todos?filter[assignee_id]=${this.config.assignee_id}&page[size]=20`,
|
|
833
|
+
token
|
|
834
|
+
);
|
|
835
|
+
const tasks = [];
|
|
836
|
+
for (const item of raw) {
|
|
837
|
+
const attrs = item.attributes;
|
|
838
|
+
if (attrs.completed_at) continue;
|
|
839
|
+
const priority = mapStretyPriority(attrs.priority);
|
|
840
|
+
const task = {
|
|
841
|
+
uuid: "",
|
|
842
|
+
description: attrs.title,
|
|
843
|
+
status: "pending",
|
|
844
|
+
entry: attrs.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
845
|
+
tags: [],
|
|
846
|
+
priority,
|
|
847
|
+
backend: "",
|
|
848
|
+
backend_id: item.id,
|
|
849
|
+
...attrs.due_date && { due: new Date(attrs.due_date).toISOString() }
|
|
850
|
+
};
|
|
851
|
+
tasks.push(task);
|
|
852
|
+
}
|
|
853
|
+
return tasks;
|
|
854
|
+
}
|
|
855
|
+
async onDone(task) {
|
|
856
|
+
const todoId = task.backend_id;
|
|
857
|
+
if (!todoId) return;
|
|
858
|
+
const token = await this.resolveToken();
|
|
859
|
+
process.stderr.write(`tw-bridge [strety]: marking todo complete
|
|
860
|
+
`);
|
|
861
|
+
await stretyFetch(`/todos/${todoId}`, token, {
|
|
862
|
+
method: "PATCH",
|
|
863
|
+
headers: { "If-Match": "*" },
|
|
864
|
+
body: JSON.stringify({
|
|
865
|
+
data: {
|
|
866
|
+
type: "todo",
|
|
867
|
+
attributes: {
|
|
868
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
})
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
function mapStretyPriority(priority) {
|
|
876
|
+
if (!priority || priority === "none") return void 0;
|
|
877
|
+
if (priority === "highest" || priority === "high") return "H";
|
|
878
|
+
if (priority === "medium") return "M";
|
|
879
|
+
if (priority === "low" || priority === "lowest") return "L";
|
|
880
|
+
return void 0;
|
|
881
|
+
}
|
|
882
|
+
|
|
575
883
|
// src/registry.ts
|
|
576
884
|
var BUILTIN_ADAPTERS = {
|
|
577
885
|
ghp: () => new GhpAdapter(),
|
|
578
|
-
asana: () => new AsanaAdapter()
|
|
886
|
+
asana: () => new AsanaAdapter(),
|
|
887
|
+
strety: () => new StretyAdapter()
|
|
579
888
|
};
|
|
580
889
|
function matchBackend(task, config) {
|
|
581
890
|
if (task.backend && config.backends[task.backend]) {
|
|
@@ -708,21 +1017,21 @@ function updateTaskDescription(existing, newDescription) {
|
|
|
708
1017
|
}
|
|
709
1018
|
|
|
710
1019
|
// src/tracking.ts
|
|
711
|
-
import
|
|
712
|
-
import
|
|
713
|
-
import
|
|
1020
|
+
import fs4 from "fs";
|
|
1021
|
+
import path5 from "path";
|
|
1022
|
+
import os4 from "os";
|
|
714
1023
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
715
|
-
var TRACKING_FILE =
|
|
1024
|
+
var TRACKING_FILE = path5.join(os4.homedir(), ".config", "tw-bridge", "tracking.json");
|
|
716
1025
|
function loadTracking() {
|
|
717
1026
|
try {
|
|
718
|
-
return JSON.parse(
|
|
1027
|
+
return JSON.parse(fs4.readFileSync(TRACKING_FILE, "utf-8"));
|
|
719
1028
|
} catch {
|
|
720
1029
|
return {};
|
|
721
1030
|
}
|
|
722
1031
|
}
|
|
723
1032
|
function saveTracking(state) {
|
|
724
|
-
|
|
725
|
-
|
|
1033
|
+
fs4.mkdirSync(path5.dirname(TRACKING_FILE), { recursive: true });
|
|
1034
|
+
fs4.writeFileSync(TRACKING_FILE, JSON.stringify(state));
|
|
726
1035
|
}
|
|
727
1036
|
function mergedTags(state) {
|
|
728
1037
|
const all = /* @__PURE__ */ new Set();
|
|
@@ -760,7 +1069,7 @@ function getActiveMeetings() {
|
|
|
760
1069
|
}
|
|
761
1070
|
|
|
762
1071
|
// src/cli.ts
|
|
763
|
-
var HOOKS_DIR =
|
|
1072
|
+
var HOOKS_DIR = path6.join(os5.homedir(), ".task", "hooks");
|
|
764
1073
|
var commands = {
|
|
765
1074
|
add: addBackend,
|
|
766
1075
|
install,
|
|
@@ -854,18 +1163,18 @@ Added backend "${name}" (adapter: ${adapterType})`);
|
|
|
854
1163
|
Use 'task context ${matchTag}' to switch to this project.`);
|
|
855
1164
|
}
|
|
856
1165
|
async function install() {
|
|
857
|
-
|
|
858
|
-
const hookSource =
|
|
859
|
-
|
|
1166
|
+
fs5.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
1167
|
+
const hookSource = path6.resolve(
|
|
1168
|
+
path6.dirname(new URL(import.meta.url).pathname),
|
|
860
1169
|
"hooks",
|
|
861
1170
|
"on-modify.js"
|
|
862
1171
|
);
|
|
863
|
-
const hookTarget =
|
|
864
|
-
if (
|
|
865
|
-
|
|
1172
|
+
const hookTarget = path6.join(HOOKS_DIR, "on-modify.tw-bridge");
|
|
1173
|
+
if (fs5.existsSync(hookTarget)) {
|
|
1174
|
+
fs5.unlinkSync(hookTarget);
|
|
866
1175
|
}
|
|
867
|
-
|
|
868
|
-
|
|
1176
|
+
fs5.symlinkSync(hookSource, hookTarget);
|
|
1177
|
+
fs5.chmodSync(hookSource, 493);
|
|
869
1178
|
console.log(`Installed hook: ${hookTarget} -> ${hookSource}`);
|
|
870
1179
|
console.log("\nAdd these to your .taskrc:\n");
|
|
871
1180
|
console.log("# --- tw-bridge UDAs ---");
|
|
@@ -882,26 +1191,26 @@ async function install() {
|
|
|
882
1191
|
console.log("urgency.user.tag.ready_for_beta.coefficient=-4.0");
|
|
883
1192
|
console.log("urgency.user.tag.in_beta.coefficient=-6.0");
|
|
884
1193
|
installTimewExtension();
|
|
885
|
-
if (
|
|
1194
|
+
if (fs5.existsSync(STANDARD_TIMEW_HOOK)) {
|
|
886
1195
|
console.log("\nTimewarrior hook detected. To enable tw-bridge time tracking:");
|
|
887
1196
|
console.log(" tw-bridge timewarrior enable");
|
|
888
1197
|
}
|
|
889
1198
|
installShellFunction();
|
|
890
1199
|
}
|
|
891
|
-
var TIMEW_EXT_DIR =
|
|
1200
|
+
var TIMEW_EXT_DIR = path6.join(os5.homedir(), ".timewarrior", "extensions");
|
|
892
1201
|
function installTimewExtension() {
|
|
893
|
-
|
|
894
|
-
const extSource =
|
|
895
|
-
|
|
1202
|
+
fs5.mkdirSync(TIMEW_EXT_DIR, { recursive: true });
|
|
1203
|
+
const extSource = path6.resolve(
|
|
1204
|
+
path6.dirname(new URL(import.meta.url).pathname),
|
|
896
1205
|
"extensions",
|
|
897
1206
|
"bridge.js"
|
|
898
1207
|
);
|
|
899
|
-
const extTarget =
|
|
900
|
-
if (
|
|
901
|
-
|
|
1208
|
+
const extTarget = path6.join(TIMEW_EXT_DIR, "bridge");
|
|
1209
|
+
if (fs5.existsSync(extTarget)) {
|
|
1210
|
+
fs5.unlinkSync(extTarget);
|
|
902
1211
|
}
|
|
903
|
-
|
|
904
|
-
|
|
1212
|
+
fs5.symlinkSync(extSource, extTarget);
|
|
1213
|
+
fs5.chmodSync(extSource, 493);
|
|
905
1214
|
console.log(`
|
|
906
1215
|
Installed Timewarrior extension: ${extTarget} -> ${extSource}`);
|
|
907
1216
|
console.log(" Usage: timew bridge [task-time|wall-time] [project-filter]");
|
|
@@ -915,8 +1224,8 @@ function detectProjectContext() {
|
|
|
915
1224
|
for (const [, backend] of Object.entries(config.backends)) {
|
|
916
1225
|
const backendCwd = backend.config?.cwd;
|
|
917
1226
|
if (!backendCwd) continue;
|
|
918
|
-
const resolved =
|
|
919
|
-
if (cwd === resolved || cwd.startsWith(resolved +
|
|
1227
|
+
const resolved = path6.resolve(backendCwd);
|
|
1228
|
+
if (cwd === resolved || cwd.startsWith(resolved + path6.sep)) {
|
|
920
1229
|
return backend.match.tags?.[0] ?? null;
|
|
921
1230
|
}
|
|
922
1231
|
}
|
|
@@ -1008,7 +1317,7 @@ async function meetingCmd() {
|
|
|
1008
1317
|
console.error(`Unknown subcommand: ${sub}`);
|
|
1009
1318
|
process.exit(1);
|
|
1010
1319
|
}
|
|
1011
|
-
var STANDARD_TIMEW_HOOK =
|
|
1320
|
+
var STANDARD_TIMEW_HOOK = path6.join(HOOKS_DIR, "on-modify.timewarrior");
|
|
1012
1321
|
async function timewarriorCmd() {
|
|
1013
1322
|
const sub = process.argv[3];
|
|
1014
1323
|
if (!sub || sub === "--help") {
|
|
@@ -1031,8 +1340,8 @@ async function timewarriorCmd() {
|
|
|
1031
1340
|
console.log("Timewarrior: enabled");
|
|
1032
1341
|
console.log(" Parallel tracking: use `task start <id> --parallel`");
|
|
1033
1342
|
}
|
|
1034
|
-
const hookExists =
|
|
1035
|
-
const hookDisabled =
|
|
1343
|
+
const hookExists = fs5.existsSync(STANDARD_TIMEW_HOOK);
|
|
1344
|
+
const hookDisabled = fs5.existsSync(STANDARD_TIMEW_HOOK + ".disabled");
|
|
1036
1345
|
if (hookExists) {
|
|
1037
1346
|
console.log(`Standard hook: active (${STANDARD_TIMEW_HOOK})`);
|
|
1038
1347
|
if (tw?.enabled) {
|
|
@@ -1050,9 +1359,9 @@ async function timewarriorCmd() {
|
|
|
1050
1359
|
const configPath = saveConfig(config);
|
|
1051
1360
|
console.log("Timewarrior tracking enabled");
|
|
1052
1361
|
console.log(`Config: ${configPath}`);
|
|
1053
|
-
if (
|
|
1362
|
+
if (fs5.existsSync(STANDARD_TIMEW_HOOK)) {
|
|
1054
1363
|
const disabled = STANDARD_TIMEW_HOOK + ".disabled";
|
|
1055
|
-
|
|
1364
|
+
fs5.renameSync(STANDARD_TIMEW_HOOK, disabled);
|
|
1056
1365
|
console.log(`
|
|
1057
1366
|
Disabled standard hook: ${STANDARD_TIMEW_HOOK} -> .disabled`);
|
|
1058
1367
|
console.log("tw-bridge will handle Timewarrior tracking directly.");
|
|
@@ -1065,8 +1374,8 @@ Disabled standard hook: ${STANDARD_TIMEW_HOOK} -> .disabled`);
|
|
|
1065
1374
|
console.log("Timewarrior tracking disabled");
|
|
1066
1375
|
console.log(`Config: ${configPath}`);
|
|
1067
1376
|
const disabled = STANDARD_TIMEW_HOOK + ".disabled";
|
|
1068
|
-
if (
|
|
1069
|
-
|
|
1377
|
+
if (fs5.existsSync(disabled)) {
|
|
1378
|
+
fs5.renameSync(disabled, STANDARD_TIMEW_HOOK);
|
|
1070
1379
|
console.log(`
|
|
1071
1380
|
Restored standard hook: ${STANDARD_TIMEW_HOOK}`);
|
|
1072
1381
|
}
|
|
@@ -1096,28 +1405,28 @@ task() {
|
|
|
1096
1405
|
var SHELL_MARKER = "# tw-bridge: auto-context task wrapper";
|
|
1097
1406
|
function installShellFunction() {
|
|
1098
1407
|
const shell = process.env.SHELL ?? "/bin/bash";
|
|
1099
|
-
const home =
|
|
1408
|
+
const home = os5.homedir();
|
|
1100
1409
|
let rcFile;
|
|
1101
1410
|
if (shell.endsWith("zsh")) {
|
|
1102
|
-
rcFile =
|
|
1411
|
+
rcFile = path6.join(home, ".zshrc");
|
|
1103
1412
|
} else {
|
|
1104
|
-
rcFile =
|
|
1413
|
+
rcFile = path6.join(home, ".bashrc");
|
|
1105
1414
|
}
|
|
1106
|
-
const existing =
|
|
1415
|
+
const existing = fs5.existsSync(rcFile) ? fs5.readFileSync(rcFile, "utf-8") : "";
|
|
1107
1416
|
if (existing.includes(SHELL_MARKER)) {
|
|
1108
1417
|
console.log(`
|
|
1109
1418
|
Shell integration already installed in ${rcFile}`);
|
|
1110
1419
|
return;
|
|
1111
1420
|
}
|
|
1112
|
-
|
|
1421
|
+
fs5.appendFileSync(rcFile, "\n" + SHELL_FUNCTION + "\n");
|
|
1113
1422
|
console.log(`
|
|
1114
1423
|
Shell integration installed in ${rcFile}`);
|
|
1115
1424
|
console.log("Restart your shell or run: source " + rcFile);
|
|
1116
1425
|
}
|
|
1117
|
-
var SEEN_FILE =
|
|
1426
|
+
var SEEN_FILE = path6.join(os5.homedir(), ".config", "tw-bridge", ".seen-dirs");
|
|
1118
1427
|
function loadSeenDirs() {
|
|
1119
1428
|
try {
|
|
1120
|
-
const raw =
|
|
1429
|
+
const raw = fs5.readFileSync(SEEN_FILE, "utf-8");
|
|
1121
1430
|
return new Set(raw.split("\n").filter(Boolean));
|
|
1122
1431
|
} catch {
|
|
1123
1432
|
return /* @__PURE__ */ new Set();
|
|
@@ -1127,15 +1436,15 @@ function markDirSeen(dir) {
|
|
|
1127
1436
|
const seen = loadSeenDirs();
|
|
1128
1437
|
if (seen.has(dir)) return;
|
|
1129
1438
|
seen.add(dir);
|
|
1130
|
-
|
|
1131
|
-
|
|
1439
|
+
fs5.mkdirSync(path6.dirname(SEEN_FILE), { recursive: true });
|
|
1440
|
+
fs5.writeFileSync(SEEN_FILE, [...seen].join("\n") + "\n");
|
|
1132
1441
|
}
|
|
1133
1442
|
function isGitRepo(dir) {
|
|
1134
1443
|
try {
|
|
1135
1444
|
let current = dir;
|
|
1136
|
-
while (current !==
|
|
1137
|
-
if (
|
|
1138
|
-
current =
|
|
1445
|
+
while (current !== path6.dirname(current)) {
|
|
1446
|
+
if (fs5.existsSync(path6.join(current, ".git"))) return true;
|
|
1447
|
+
current = path6.dirname(current);
|
|
1139
1448
|
}
|
|
1140
1449
|
return false;
|
|
1141
1450
|
} catch {
|
|
@@ -1148,8 +1457,8 @@ async function which() {
|
|
|
1148
1457
|
for (const [_name, backend] of Object.entries(config.backends)) {
|
|
1149
1458
|
const backendCwd = backend.config?.cwd;
|
|
1150
1459
|
if (!backendCwd) continue;
|
|
1151
|
-
const resolved =
|
|
1152
|
-
if (cwd === resolved || cwd.startsWith(resolved +
|
|
1460
|
+
const resolved = path6.resolve(backendCwd);
|
|
1461
|
+
if (cwd === resolved || cwd.startsWith(resolved + path6.sep)) {
|
|
1153
1462
|
const contextTag = backend.match.tags?.[0];
|
|
1154
1463
|
if (contextTag) {
|
|
1155
1464
|
process.stdout.write(contextTag);
|
|
@@ -1165,15 +1474,15 @@ async function which() {
|
|
|
1165
1474
|
const seen = loadSeenDirs();
|
|
1166
1475
|
let gitRoot = cwd;
|
|
1167
1476
|
let current = cwd;
|
|
1168
|
-
while (current !==
|
|
1169
|
-
if (
|
|
1477
|
+
while (current !== path6.dirname(current)) {
|
|
1478
|
+
if (fs5.existsSync(path6.join(current, ".git"))) {
|
|
1170
1479
|
gitRoot = current;
|
|
1171
1480
|
break;
|
|
1172
1481
|
}
|
|
1173
|
-
current =
|
|
1482
|
+
current = path6.dirname(current);
|
|
1174
1483
|
}
|
|
1175
1484
|
if (!seen.has(gitRoot)) {
|
|
1176
|
-
const dirName =
|
|
1485
|
+
const dirName = path6.basename(gitRoot);
|
|
1177
1486
|
process.stderr.write(
|
|
1178
1487
|
`tw-bridge: unconfigured project "${dirName}". Run: tw-bridge add ${dirName} --adapter ghp
|
|
1179
1488
|
`
|
package/dist/hooks/on-modify.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/hooks/on-modify.ts
|
|
4
|
-
import
|
|
4
|
+
import fs5 from "fs";
|
|
5
5
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
6
6
|
|
|
7
7
|
// src/config.ts
|
|
@@ -234,8 +234,8 @@ async function oauthLogin(clientId, clientSecret, rl) {
|
|
|
234
234
|
saveTokens(tokens);
|
|
235
235
|
return tokens;
|
|
236
236
|
}
|
|
237
|
-
async function asanaFetch(
|
|
238
|
-
const url = `${API_BASE}${
|
|
237
|
+
async function asanaFetch(path6, token, options = {}) {
|
|
238
|
+
const url = `${API_BASE}${path6}`;
|
|
239
239
|
const res = await fetch(url, {
|
|
240
240
|
...options,
|
|
241
241
|
headers: {
|
|
@@ -251,9 +251,9 @@ async function asanaFetch(path5, token, options = {}) {
|
|
|
251
251
|
const json = await res.json();
|
|
252
252
|
return json.data;
|
|
253
253
|
}
|
|
254
|
-
async function asanaFetchAll(
|
|
254
|
+
async function asanaFetchAll(path6, token) {
|
|
255
255
|
const results = [];
|
|
256
|
-
let nextPage =
|
|
256
|
+
let nextPage = path6;
|
|
257
257
|
while (nextPage) {
|
|
258
258
|
const url = `${API_BASE}${nextPage}`;
|
|
259
259
|
const res = await fetch(url, {
|
|
@@ -565,10 +565,319 @@ function mapAsanaPriority(fields) {
|
|
|
565
565
|
return void 0;
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
+
// src/adapters/strety.ts
|
|
569
|
+
import fs3 from "fs";
|
|
570
|
+
import path4 from "path";
|
|
571
|
+
import os3 from "os";
|
|
572
|
+
import { execSync as execSync2 } from "child_process";
|
|
573
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
574
|
+
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
575
|
+
var API_BASE2 = "https://2.strety.com/api/v1";
|
|
576
|
+
var OAUTH_AUTHORIZE2 = `${API_BASE2}/oauth/authorize`;
|
|
577
|
+
var OAUTH_TOKEN2 = `${API_BASE2}/oauth/token`;
|
|
578
|
+
var TOKEN_FILE2 = path4.join(os3.homedir(), ".config", "tw-bridge", "strety-tokens.json");
|
|
579
|
+
function loadTokens2() {
|
|
580
|
+
try {
|
|
581
|
+
return JSON.parse(fs3.readFileSync(TOKEN_FILE2, "utf-8"));
|
|
582
|
+
} catch {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function saveTokens2(tokens) {
|
|
587
|
+
fs3.mkdirSync(path4.dirname(TOKEN_FILE2), { recursive: true });
|
|
588
|
+
fs3.writeFileSync(TOKEN_FILE2, JSON.stringify(tokens, null, 2), { mode: 384 });
|
|
589
|
+
}
|
|
590
|
+
async function refreshAccessToken2(tokens) {
|
|
591
|
+
const now = Date.now();
|
|
592
|
+
if (tokens.access_token && tokens.expires_at > now + 3e5) {
|
|
593
|
+
return tokens.access_token;
|
|
594
|
+
}
|
|
595
|
+
const res = await fetch(OAUTH_TOKEN2, {
|
|
596
|
+
method: "POST",
|
|
597
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
598
|
+
body: new URLSearchParams({
|
|
599
|
+
grant_type: "refresh_token",
|
|
600
|
+
refresh_token: tokens.refresh_token,
|
|
601
|
+
client_id: tokens.client_id,
|
|
602
|
+
client_secret: tokens.client_secret
|
|
603
|
+
})
|
|
604
|
+
});
|
|
605
|
+
if (!res.ok) {
|
|
606
|
+
const body = await res.text();
|
|
607
|
+
throw new Error(`Token refresh failed (${res.status}): ${body}`);
|
|
608
|
+
}
|
|
609
|
+
const data = await res.json();
|
|
610
|
+
tokens.access_token = data.access_token;
|
|
611
|
+
if (data.refresh_token) {
|
|
612
|
+
tokens.refresh_token = data.refresh_token;
|
|
613
|
+
}
|
|
614
|
+
tokens.expires_at = now + (data.expires_in ?? 7200) * 1e3;
|
|
615
|
+
saveTokens2(tokens);
|
|
616
|
+
return tokens.access_token;
|
|
617
|
+
}
|
|
618
|
+
var REDIRECT_URI = "https://localhost/callback";
|
|
619
|
+
async function oauthLogin2(clientId, clientSecret, rl) {
|
|
620
|
+
const authUrl = `${OAUTH_AUTHORIZE2}?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=${encodeURIComponent("read write")}`;
|
|
621
|
+
console.log(`
|
|
622
|
+
Opening browser for authorization...`);
|
|
623
|
+
console.log(` If it doesn't open, visit:
|
|
624
|
+
${authUrl}
|
|
625
|
+
`);
|
|
626
|
+
try {
|
|
627
|
+
execSync2(`xdg-open "${authUrl}" 2>/dev/null || open "${authUrl}" 2>/dev/null`, {
|
|
628
|
+
stdio: "ignore"
|
|
629
|
+
});
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
console.log(" After authorizing, the browser will redirect to a page that");
|
|
633
|
+
console.log(" won't load. Copy the full URL from the address bar and paste it here.\n");
|
|
634
|
+
const pasted = (await rl.question("Paste the callback URL: ")).trim();
|
|
635
|
+
let code;
|
|
636
|
+
try {
|
|
637
|
+
const url = new URL(pasted);
|
|
638
|
+
const error = url.searchParams.get("error");
|
|
639
|
+
if (error) {
|
|
640
|
+
throw new Error(`OAuth error: ${error}`);
|
|
641
|
+
}
|
|
642
|
+
const authCode = url.searchParams.get("code");
|
|
643
|
+
if (!authCode) {
|
|
644
|
+
throw new Error("No authorization code found in URL");
|
|
645
|
+
}
|
|
646
|
+
code = authCode;
|
|
647
|
+
} catch (err) {
|
|
648
|
+
if (err instanceof Error && err.message.startsWith("OAuth error:")) throw err;
|
|
649
|
+
if (err instanceof Error && err.message.startsWith("No authorization")) throw err;
|
|
650
|
+
throw new Error(`Could not parse callback URL: ${pasted}`);
|
|
651
|
+
}
|
|
652
|
+
const tokenRes = await fetch(OAUTH_TOKEN2, {
|
|
653
|
+
method: "POST",
|
|
654
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
655
|
+
body: new URLSearchParams({
|
|
656
|
+
grant_type: "authorization_code",
|
|
657
|
+
code,
|
|
658
|
+
redirect_uri: REDIRECT_URI,
|
|
659
|
+
client_id: clientId,
|
|
660
|
+
client_secret: clientSecret
|
|
661
|
+
})
|
|
662
|
+
});
|
|
663
|
+
if (!tokenRes.ok) {
|
|
664
|
+
const body = await tokenRes.text();
|
|
665
|
+
throw new Error(`Token exchange failed (${tokenRes.status}): ${body}`);
|
|
666
|
+
}
|
|
667
|
+
const data = await tokenRes.json();
|
|
668
|
+
const tokens = {
|
|
669
|
+
access_token: data.access_token,
|
|
670
|
+
refresh_token: data.refresh_token,
|
|
671
|
+
expires_at: Date.now() + (data.expires_in ?? 7200) * 1e3,
|
|
672
|
+
client_id: clientId,
|
|
673
|
+
client_secret: clientSecret
|
|
674
|
+
};
|
|
675
|
+
saveTokens2(tokens);
|
|
676
|
+
return tokens;
|
|
677
|
+
}
|
|
678
|
+
async function stretyFetch(urlOrPath, token, options = {}) {
|
|
679
|
+
const url = urlOrPath.startsWith("http") ? urlOrPath : `${API_BASE2}${urlOrPath}`;
|
|
680
|
+
const res = await fetch(url, {
|
|
681
|
+
...options,
|
|
682
|
+
headers: {
|
|
683
|
+
Authorization: `Bearer ${token}`,
|
|
684
|
+
Accept: "application/vnd.api+json",
|
|
685
|
+
"Content-Type": "application/vnd.api+json",
|
|
686
|
+
...options.headers
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
if (!res.ok) {
|
|
690
|
+
const body = await res.text();
|
|
691
|
+
throw new Error(`Strety API ${res.status}: ${body}`);
|
|
692
|
+
}
|
|
693
|
+
return res.json();
|
|
694
|
+
}
|
|
695
|
+
async function stretyFetchAll(path6, token) {
|
|
696
|
+
const results = [];
|
|
697
|
+
let url = `${API_BASE2}${path6}`;
|
|
698
|
+
while (url) {
|
|
699
|
+
const json = await stretyFetch(url, token);
|
|
700
|
+
results.push(...json.data);
|
|
701
|
+
url = json.links?.next ?? null;
|
|
702
|
+
}
|
|
703
|
+
return results;
|
|
704
|
+
}
|
|
705
|
+
async function pickOne2(rl, label, items) {
|
|
706
|
+
console.log(`
|
|
707
|
+
${label}:`);
|
|
708
|
+
for (let i = 0; i < items.length; i++) {
|
|
709
|
+
const name = items[i].attributes.name ?? items[i].attributes.title ?? items[i].id;
|
|
710
|
+
console.log(` ${i + 1}) ${name}`);
|
|
711
|
+
}
|
|
712
|
+
while (true) {
|
|
713
|
+
const answer = await rl.question(`Choose (1-${items.length}): `);
|
|
714
|
+
const idx = parseInt(answer, 10) - 1;
|
|
715
|
+
if (idx >= 0 && idx < items.length) {
|
|
716
|
+
const item = items[idx];
|
|
717
|
+
return { id: item.id, name: item.attributes.name ?? item.attributes.title ?? item.id };
|
|
718
|
+
}
|
|
719
|
+
console.log(" Invalid choice, try again.");
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
var StretyAdapter = class {
|
|
723
|
+
name = "strety";
|
|
724
|
+
config;
|
|
725
|
+
accessToken = null;
|
|
726
|
+
async resolveToken() {
|
|
727
|
+
const auth = this.config.auth;
|
|
728
|
+
if (auth === "oauth") {
|
|
729
|
+
const tokens = loadTokens2();
|
|
730
|
+
if (!tokens) throw new Error("No OAuth tokens found. Run `tw-bridge add` to authenticate.");
|
|
731
|
+
this.accessToken = await refreshAccessToken2(tokens);
|
|
732
|
+
return this.accessToken;
|
|
733
|
+
}
|
|
734
|
+
if (this.accessToken) return this.accessToken;
|
|
735
|
+
if (auth.startsWith("env:")) {
|
|
736
|
+
const envVar = auth.slice(4);
|
|
737
|
+
const val = process.env[envVar];
|
|
738
|
+
if (!val) throw new Error(`Environment variable ${envVar} not set`);
|
|
739
|
+
this.accessToken = val;
|
|
740
|
+
return val;
|
|
741
|
+
}
|
|
742
|
+
this.accessToken = auth;
|
|
743
|
+
return auth;
|
|
744
|
+
}
|
|
745
|
+
defaultConfig(_cwd) {
|
|
746
|
+
return {
|
|
747
|
+
auth: "env:STRETY_TOKEN",
|
|
748
|
+
assignee_id: "",
|
|
749
|
+
assignee_name: ""
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
async setup(_cwd) {
|
|
753
|
+
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
754
|
+
try {
|
|
755
|
+
console.log("\n\u2500\u2500 Strety Adapter Setup \u2500\u2500\n");
|
|
756
|
+
console.log("Authentication:");
|
|
757
|
+
console.log(" 1) OAuth (browser login)");
|
|
758
|
+
console.log(" 2) Environment variable");
|
|
759
|
+
const authChoice = (await rl.question("\nChoose (1-2): ")).trim();
|
|
760
|
+
let authConfig;
|
|
761
|
+
let token;
|
|
762
|
+
if (authChoice === "1") {
|
|
763
|
+
console.log(`
|
|
764
|
+
Register a Strety app at My Integrations > My Apps.`);
|
|
765
|
+
console.log(` Set the Redirect URI to: ${REDIRECT_URI}
|
|
766
|
+
`);
|
|
767
|
+
const clientId = (await rl.question("Strety App Client ID: ")).trim();
|
|
768
|
+
const clientSecret = (await rl.question("Strety App Client Secret: ")).trim();
|
|
769
|
+
if (!clientId || !clientSecret) {
|
|
770
|
+
console.error("\n Client ID and Secret are required.");
|
|
771
|
+
rl.close();
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
const tokens = await oauthLogin2(clientId, clientSecret, rl);
|
|
775
|
+
token = tokens.access_token;
|
|
776
|
+
authConfig = "oauth";
|
|
777
|
+
} else {
|
|
778
|
+
const envVar = await rl.question("Environment variable name [STRETY_TOKEN]: ");
|
|
779
|
+
const varName = envVar.trim() || "STRETY_TOKEN";
|
|
780
|
+
authConfig = `env:${varName}`;
|
|
781
|
+
token = process.env[varName] ?? "";
|
|
782
|
+
if (!token) {
|
|
783
|
+
console.error(`
|
|
784
|
+
Cannot complete setup without a valid token.`);
|
|
785
|
+
console.error(` Set ${varName} in your environment and try again.`);
|
|
786
|
+
rl.close();
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
console.log("\nVerifying...");
|
|
791
|
+
let people;
|
|
792
|
+
try {
|
|
793
|
+
people = await stretyFetchAll("/people?page[size]=20", token);
|
|
794
|
+
} catch (err) {
|
|
795
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
796
|
+
console.error(` Authentication failed: ${msg}`);
|
|
797
|
+
rl.close();
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
console.log(` Connected! Found ${people.length} people.`);
|
|
801
|
+
const person = await pickOne2(rl, "Which person are you? (for filtering your todos)", people);
|
|
802
|
+
rl.close();
|
|
803
|
+
console.log(`
|
|
804
|
+
Assignee: ${person.name}`);
|
|
805
|
+
console.log(" Syncing: Todos assigned to you");
|
|
806
|
+
return {
|
|
807
|
+
auth: authConfig,
|
|
808
|
+
assignee_id: person.id,
|
|
809
|
+
assignee_name: person.name
|
|
810
|
+
};
|
|
811
|
+
} catch (err) {
|
|
812
|
+
rl.close();
|
|
813
|
+
throw err;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async init(config) {
|
|
817
|
+
this.config = config;
|
|
818
|
+
if (!this.config.assignee_id) {
|
|
819
|
+
throw new Error('strety adapter requires "assignee_id" in config');
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async pull() {
|
|
823
|
+
const token = await this.resolveToken();
|
|
824
|
+
const raw = await stretyFetchAll(
|
|
825
|
+
`/todos?filter[assignee_id]=${this.config.assignee_id}&page[size]=20`,
|
|
826
|
+
token
|
|
827
|
+
);
|
|
828
|
+
const tasks = [];
|
|
829
|
+
for (const item of raw) {
|
|
830
|
+
const attrs = item.attributes;
|
|
831
|
+
if (attrs.completed_at) continue;
|
|
832
|
+
const priority = mapStretyPriority(attrs.priority);
|
|
833
|
+
const task = {
|
|
834
|
+
uuid: "",
|
|
835
|
+
description: attrs.title,
|
|
836
|
+
status: "pending",
|
|
837
|
+
entry: attrs.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
838
|
+
tags: [],
|
|
839
|
+
priority,
|
|
840
|
+
backend: "",
|
|
841
|
+
backend_id: item.id,
|
|
842
|
+
...attrs.due_date && { due: new Date(attrs.due_date).toISOString() }
|
|
843
|
+
};
|
|
844
|
+
tasks.push(task);
|
|
845
|
+
}
|
|
846
|
+
return tasks;
|
|
847
|
+
}
|
|
848
|
+
async onDone(task) {
|
|
849
|
+
const todoId = task.backend_id;
|
|
850
|
+
if (!todoId) return;
|
|
851
|
+
const token = await this.resolveToken();
|
|
852
|
+
process.stderr.write(`tw-bridge [strety]: marking todo complete
|
|
853
|
+
`);
|
|
854
|
+
await stretyFetch(`/todos/${todoId}`, token, {
|
|
855
|
+
method: "PATCH",
|
|
856
|
+
headers: { "If-Match": "*" },
|
|
857
|
+
body: JSON.stringify({
|
|
858
|
+
data: {
|
|
859
|
+
type: "todo",
|
|
860
|
+
attributes: {
|
|
861
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
})
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
function mapStretyPriority(priority) {
|
|
869
|
+
if (!priority || priority === "none") return void 0;
|
|
870
|
+
if (priority === "highest" || priority === "high") return "H";
|
|
871
|
+
if (priority === "medium") return "M";
|
|
872
|
+
if (priority === "low" || priority === "lowest") return "L";
|
|
873
|
+
return void 0;
|
|
874
|
+
}
|
|
875
|
+
|
|
568
876
|
// src/registry.ts
|
|
569
877
|
var BUILTIN_ADAPTERS = {
|
|
570
878
|
ghp: () => new GhpAdapter(),
|
|
571
|
-
asana: () => new AsanaAdapter()
|
|
879
|
+
asana: () => new AsanaAdapter(),
|
|
880
|
+
strety: () => new StretyAdapter()
|
|
572
881
|
};
|
|
573
882
|
function matchBackend(task, config) {
|
|
574
883
|
if (task.backend && config.backends[task.backend]) {
|
|
@@ -600,21 +909,21 @@ async function resolveAdapter(task, config) {
|
|
|
600
909
|
}
|
|
601
910
|
|
|
602
911
|
// src/tracking.ts
|
|
603
|
-
import
|
|
604
|
-
import
|
|
605
|
-
import
|
|
912
|
+
import fs4 from "fs";
|
|
913
|
+
import path5 from "path";
|
|
914
|
+
import os4 from "os";
|
|
606
915
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
607
|
-
var TRACKING_FILE =
|
|
916
|
+
var TRACKING_FILE = path5.join(os4.homedir(), ".config", "tw-bridge", "tracking.json");
|
|
608
917
|
function loadTracking() {
|
|
609
918
|
try {
|
|
610
|
-
return JSON.parse(
|
|
919
|
+
return JSON.parse(fs4.readFileSync(TRACKING_FILE, "utf-8"));
|
|
611
920
|
} catch {
|
|
612
921
|
return {};
|
|
613
922
|
}
|
|
614
923
|
}
|
|
615
924
|
function saveTracking(state) {
|
|
616
|
-
|
|
617
|
-
|
|
925
|
+
fs4.mkdirSync(path5.dirname(TRACKING_FILE), { recursive: true });
|
|
926
|
+
fs4.writeFileSync(TRACKING_FILE, JSON.stringify(state));
|
|
618
927
|
}
|
|
619
928
|
function mergedTags(state) {
|
|
620
929
|
const all = /* @__PURE__ */ new Set();
|
|
@@ -689,10 +998,10 @@ tw-bridge: Currently tracking: ${tags} (${duration})`
|
|
|
689
998
|
lines.push(" [s] Switch \u2014 stop current, start new task (default)");
|
|
690
999
|
lines.push(" [p] Parallel \u2014 add new task to current tracking");
|
|
691
1000
|
lines.push("");
|
|
692
|
-
|
|
693
|
-
|
|
1001
|
+
fs5.writeSync(ttyFd, lines.join("\n"));
|
|
1002
|
+
fs5.writeSync(ttyFd, " > ");
|
|
694
1003
|
const buf = Buffer.alloc(64);
|
|
695
|
-
const bytesRead =
|
|
1004
|
+
const bytesRead = fs5.readSync(ttyFd, buf, 0, 64, null);
|
|
696
1005
|
const answer = buf.toString("utf-8", 0, bytesRead).trim().toLowerCase();
|
|
697
1006
|
return answer.startsWith("p") ? "parallel" : "switch";
|
|
698
1007
|
}
|
|
@@ -738,7 +1047,7 @@ function handleTimewarriorStop(config, task) {
|
|
|
738
1047
|
}
|
|
739
1048
|
}
|
|
740
1049
|
async function main() {
|
|
741
|
-
const input =
|
|
1050
|
+
const input = fs5.readFileSync("/dev/stdin", "utf-8").trim().split("\n");
|
|
742
1051
|
const oldTask = JSON.parse(input[0]);
|
|
743
1052
|
const newTask = JSON.parse(input[1]);
|
|
744
1053
|
process.stdout.write(JSON.stringify(newTask) + "\n");
|
|
@@ -749,7 +1058,7 @@ async function main() {
|
|
|
749
1058
|
let ttyFd = null;
|
|
750
1059
|
if (wasStarted) {
|
|
751
1060
|
try {
|
|
752
|
-
ttyFd =
|
|
1061
|
+
ttyFd = fs5.openSync("/dev/tty", "r+");
|
|
753
1062
|
} catch {
|
|
754
1063
|
}
|
|
755
1064
|
}
|
|
@@ -777,7 +1086,7 @@ async function main() {
|
|
|
777
1086
|
process.stderr.write(`tw-bridge: ${msg}
|
|
778
1087
|
`);
|
|
779
1088
|
} finally {
|
|
780
|
-
if (ttyFd !== null)
|
|
1089
|
+
if (ttyFd !== null) fs5.closeSync(ttyFd);
|
|
781
1090
|
}
|
|
782
1091
|
}
|
|
783
1092
|
main();
|