@bretwardjames/tw-bridge 0.4.0 → 0.5.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 +209 -53
- package/dist/hooks/on-modify.js +34 -30
- 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 fs4 from "fs";
|
|
5
|
+
import path5 from "path";
|
|
6
|
+
import os4 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(path6, token, options = {}) {
|
|
245
|
+
const url = `${API_BASE}${path6}`;
|
|
246
246
|
const res = await fetch(url, {
|
|
247
247
|
...options,
|
|
248
248
|
headers: {
|
|
@@ -258,9 +258,9 @@ async function asanaFetch(path5, token, options = {}) {
|
|
|
258
258
|
const json = await res.json();
|
|
259
259
|
return json.data;
|
|
260
260
|
}
|
|
261
|
-
async function asanaFetchAll(
|
|
261
|
+
async function asanaFetchAll(path6, token) {
|
|
262
262
|
const results = [];
|
|
263
|
-
let nextPage =
|
|
263
|
+
let nextPage = path6;
|
|
264
264
|
while (nextPage) {
|
|
265
265
|
const url = `${API_BASE}${nextPage}`;
|
|
266
266
|
const res = await fetch(url, {
|
|
@@ -707,12 +707,65 @@ function updateTaskDescription(existing, newDescription) {
|
|
|
707
707
|
return result.status === 0;
|
|
708
708
|
}
|
|
709
709
|
|
|
710
|
+
// src/tracking.ts
|
|
711
|
+
import fs3 from "fs";
|
|
712
|
+
import path4 from "path";
|
|
713
|
+
import os3 from "os";
|
|
714
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
715
|
+
var TRACKING_FILE = path4.join(os3.homedir(), ".config", "tw-bridge", "tracking.json");
|
|
716
|
+
function loadTracking() {
|
|
717
|
+
try {
|
|
718
|
+
return JSON.parse(fs3.readFileSync(TRACKING_FILE, "utf-8"));
|
|
719
|
+
} catch {
|
|
720
|
+
return {};
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function saveTracking(state) {
|
|
724
|
+
fs3.mkdirSync(path4.dirname(TRACKING_FILE), { recursive: true });
|
|
725
|
+
fs3.writeFileSync(TRACKING_FILE, JSON.stringify(state));
|
|
726
|
+
}
|
|
727
|
+
function mergedTags(state) {
|
|
728
|
+
const all = /* @__PURE__ */ new Set();
|
|
729
|
+
for (const tags of Object.values(state)) {
|
|
730
|
+
for (const tag of tags) all.add(tag);
|
|
731
|
+
}
|
|
732
|
+
return [...all];
|
|
733
|
+
}
|
|
734
|
+
function startParallel(key, tags) {
|
|
735
|
+
const tracking = loadTracking();
|
|
736
|
+
tracking[key] = tags;
|
|
737
|
+
saveTracking(tracking);
|
|
738
|
+
const merged = mergedTags(tracking);
|
|
739
|
+
spawnSync3("timew", ["stop"], { stdio: "pipe" });
|
|
740
|
+
spawnSync3("timew", ["start", ...merged], { stdio: "pipe" });
|
|
741
|
+
}
|
|
742
|
+
function startSwitch(key, tags) {
|
|
743
|
+
saveTracking({ [key]: tags });
|
|
744
|
+
spawnSync3("timew", ["stop"], { stdio: "pipe" });
|
|
745
|
+
spawnSync3("timew", ["start", ...tags], { stdio: "pipe" });
|
|
746
|
+
}
|
|
747
|
+
function stopEntry(key) {
|
|
748
|
+
const tracking = loadTracking();
|
|
749
|
+
delete tracking[key];
|
|
750
|
+
saveTracking(tracking);
|
|
751
|
+
const remaining = mergedTags(tracking);
|
|
752
|
+
spawnSync3("timew", ["stop"], { stdio: "pipe" });
|
|
753
|
+
if (remaining.length > 0) {
|
|
754
|
+
spawnSync3("timew", ["start", ...remaining], { stdio: "pipe" });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function getActiveMeetings() {
|
|
758
|
+
const tracking = loadTracking();
|
|
759
|
+
return Object.entries(tracking).filter(([key]) => key.startsWith("meeting:")).map(([key, tags]) => ({ key, tags }));
|
|
760
|
+
}
|
|
761
|
+
|
|
710
762
|
// src/cli.ts
|
|
711
|
-
var HOOKS_DIR =
|
|
763
|
+
var HOOKS_DIR = path5.join(os4.homedir(), ".task", "hooks");
|
|
712
764
|
var commands = {
|
|
713
765
|
add: addBackend,
|
|
714
766
|
install,
|
|
715
767
|
sync,
|
|
768
|
+
meeting: meetingCmd,
|
|
716
769
|
timewarrior: timewarriorCmd,
|
|
717
770
|
which,
|
|
718
771
|
config: showConfig
|
|
@@ -722,9 +775,10 @@ async function main() {
|
|
|
722
775
|
if (!command || command === "--help") {
|
|
723
776
|
console.log("Usage: tw-bridge <command>\n");
|
|
724
777
|
console.log("Commands:");
|
|
725
|
-
console.log(" add
|
|
726
|
-
console.log(" install
|
|
778
|
+
console.log(" add Add a new backend instance");
|
|
779
|
+
console.log(" install Install Taskwarrior hooks and shell integration");
|
|
727
780
|
console.log(" sync Pull tasks from all backends");
|
|
781
|
+
console.log(" meeting Track meetings in Timewarrior (no task created)");
|
|
728
782
|
console.log(" timewarrior Manage Timewarrior integration");
|
|
729
783
|
console.log(" which Print the context for the current directory");
|
|
730
784
|
console.log(" config Show current configuration");
|
|
@@ -800,18 +854,18 @@ Added backend "${name}" (adapter: ${adapterType})`);
|
|
|
800
854
|
Use 'task context ${matchTag}' to switch to this project.`);
|
|
801
855
|
}
|
|
802
856
|
async function install() {
|
|
803
|
-
|
|
804
|
-
const hookSource =
|
|
805
|
-
|
|
857
|
+
fs4.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
858
|
+
const hookSource = path5.resolve(
|
|
859
|
+
path5.dirname(new URL(import.meta.url).pathname),
|
|
806
860
|
"hooks",
|
|
807
861
|
"on-modify.js"
|
|
808
862
|
);
|
|
809
|
-
const hookTarget =
|
|
810
|
-
if (
|
|
811
|
-
|
|
863
|
+
const hookTarget = path5.join(HOOKS_DIR, "on-modify.tw-bridge");
|
|
864
|
+
if (fs4.existsSync(hookTarget)) {
|
|
865
|
+
fs4.unlinkSync(hookTarget);
|
|
812
866
|
}
|
|
813
|
-
|
|
814
|
-
|
|
867
|
+
fs4.symlinkSync(hookSource, hookTarget);
|
|
868
|
+
fs4.chmodSync(hookSource, 493);
|
|
815
869
|
console.log(`Installed hook: ${hookTarget} -> ${hookSource}`);
|
|
816
870
|
console.log("\nAdd these to your .taskrc:\n");
|
|
817
871
|
console.log("# --- tw-bridge UDAs ---");
|
|
@@ -828,31 +882,133 @@ async function install() {
|
|
|
828
882
|
console.log("urgency.user.tag.ready_for_beta.coefficient=-4.0");
|
|
829
883
|
console.log("urgency.user.tag.in_beta.coefficient=-6.0");
|
|
830
884
|
installTimewExtension();
|
|
831
|
-
if (
|
|
885
|
+
if (fs4.existsSync(STANDARD_TIMEW_HOOK)) {
|
|
832
886
|
console.log("\nTimewarrior hook detected. To enable tw-bridge time tracking:");
|
|
833
887
|
console.log(" tw-bridge timewarrior enable");
|
|
834
888
|
}
|
|
835
889
|
installShellFunction();
|
|
836
890
|
}
|
|
837
|
-
var TIMEW_EXT_DIR =
|
|
891
|
+
var TIMEW_EXT_DIR = path5.join(os4.homedir(), ".timewarrior", "extensions");
|
|
838
892
|
function installTimewExtension() {
|
|
839
|
-
|
|
840
|
-
const extSource =
|
|
841
|
-
|
|
893
|
+
fs4.mkdirSync(TIMEW_EXT_DIR, { recursive: true });
|
|
894
|
+
const extSource = path5.resolve(
|
|
895
|
+
path5.dirname(new URL(import.meta.url).pathname),
|
|
842
896
|
"extensions",
|
|
843
897
|
"bridge.js"
|
|
844
898
|
);
|
|
845
|
-
const extTarget =
|
|
846
|
-
if (
|
|
847
|
-
|
|
899
|
+
const extTarget = path5.join(TIMEW_EXT_DIR, "bridge");
|
|
900
|
+
if (fs4.existsSync(extTarget)) {
|
|
901
|
+
fs4.unlinkSync(extTarget);
|
|
848
902
|
}
|
|
849
|
-
|
|
850
|
-
|
|
903
|
+
fs4.symlinkSync(extSource, extTarget);
|
|
904
|
+
fs4.chmodSync(extSource, 493);
|
|
851
905
|
console.log(`
|
|
852
906
|
Installed Timewarrior extension: ${extTarget} -> ${extSource}`);
|
|
853
907
|
console.log(" Usage: timew bridge [task-time|wall-time] [project-filter]");
|
|
854
908
|
}
|
|
855
|
-
|
|
909
|
+
function sanitizeMeetingName(name) {
|
|
910
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
911
|
+
}
|
|
912
|
+
function detectProjectContext() {
|
|
913
|
+
const cwd = process.cwd();
|
|
914
|
+
const config = loadConfig();
|
|
915
|
+
for (const [, backend] of Object.entries(config.backends)) {
|
|
916
|
+
const backendCwd = backend.config?.cwd;
|
|
917
|
+
if (!backendCwd) continue;
|
|
918
|
+
const resolved = path5.resolve(backendCwd);
|
|
919
|
+
if (cwd === resolved || cwd.startsWith(resolved + path5.sep)) {
|
|
920
|
+
return backend.match.tags?.[0] ?? null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
async function meetingCmd() {
|
|
926
|
+
const sub = process.argv[3];
|
|
927
|
+
if (!sub || sub === "--help") {
|
|
928
|
+
console.log("Usage: tw-bridge meeting <subcommand>\n");
|
|
929
|
+
console.log("Subcommands:");
|
|
930
|
+
console.log(" start <name> [--switch] Start tracking a meeting");
|
|
931
|
+
console.log(" stop [name] Stop a meeting (or all if no name)");
|
|
932
|
+
console.log(" list Show active meetings");
|
|
933
|
+
console.log("\nMeetings are tracked in Timewarrior only \u2014 no Taskwarrior task is created.");
|
|
934
|
+
console.log("By default, meetings run in parallel with active tasks.");
|
|
935
|
+
console.log("Use --switch to pause the current task instead.");
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const config = loadConfig();
|
|
939
|
+
if (!config.timewarrior?.enabled) {
|
|
940
|
+
console.error("Timewarrior is not enabled. Run: tw-bridge timewarrior enable");
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
if (sub === "start") {
|
|
944
|
+
const nameArg = process.argv.slice(4).filter((a) => !a.startsWith("--")).join(" ");
|
|
945
|
+
if (!nameArg) {
|
|
946
|
+
console.error("Usage: tw-bridge meeting start <name>");
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
const switchMode = process.argv.includes("--switch") || process.argv.includes("-s");
|
|
950
|
+
const tag = sanitizeMeetingName(nameArg);
|
|
951
|
+
const key = `meeting:${tag}`;
|
|
952
|
+
const tags = ["meeting", tag];
|
|
953
|
+
const project = detectProjectContext();
|
|
954
|
+
if (project) tags.push(project);
|
|
955
|
+
if (switchMode) {
|
|
956
|
+
startSwitch(key, tags);
|
|
957
|
+
} else {
|
|
958
|
+
startParallel(key, tags);
|
|
959
|
+
}
|
|
960
|
+
console.log(`Meeting started: ${nameArg}`);
|
|
961
|
+
console.log(` Tags: ${tags.join(" ")}`);
|
|
962
|
+
if (!switchMode) {
|
|
963
|
+
console.log(" Mode: parallel (active tasks continue tracking)");
|
|
964
|
+
} else {
|
|
965
|
+
console.log(" Mode: switch (active tasks paused)");
|
|
966
|
+
}
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (sub === "stop") {
|
|
970
|
+
const nameArg = process.argv.slice(4).join(" ").trim();
|
|
971
|
+
const active = getActiveMeetings();
|
|
972
|
+
if (active.length === 0) {
|
|
973
|
+
console.log("No active meetings.");
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (nameArg) {
|
|
977
|
+
const tag = sanitizeMeetingName(nameArg);
|
|
978
|
+
const key = `meeting:${tag}`;
|
|
979
|
+
const match = active.find((m) => m.key === key);
|
|
980
|
+
if (!match) {
|
|
981
|
+
console.error(`No active meeting matching "${nameArg}".`);
|
|
982
|
+
console.error(`Active meetings: ${active.map((m) => m.key.replace("meeting:", "")).join(", ")}`);
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
stopEntry(key);
|
|
986
|
+
console.log(`Meeting stopped: ${nameArg}`);
|
|
987
|
+
} else {
|
|
988
|
+
for (const m of active) {
|
|
989
|
+
stopEntry(m.key);
|
|
990
|
+
}
|
|
991
|
+
console.log(`Stopped ${active.length} meeting(s): ${active.map((m) => m.key.replace("meeting:", "")).join(", ")}`);
|
|
992
|
+
}
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (sub === "list") {
|
|
996
|
+
const active = getActiveMeetings();
|
|
997
|
+
if (active.length === 0) {
|
|
998
|
+
console.log("No active meetings.");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
console.log("Active meetings:");
|
|
1002
|
+
for (const m of active) {
|
|
1003
|
+
const name = m.key.replace("meeting:", "");
|
|
1004
|
+
console.log(` ${name} (${m.tags.join(" ")})`);
|
|
1005
|
+
}
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
1009
|
+
process.exit(1);
|
|
1010
|
+
}
|
|
1011
|
+
var STANDARD_TIMEW_HOOK = path5.join(HOOKS_DIR, "on-modify.timewarrior");
|
|
856
1012
|
async function timewarriorCmd() {
|
|
857
1013
|
const sub = process.argv[3];
|
|
858
1014
|
if (!sub || sub === "--help") {
|
|
@@ -875,8 +1031,8 @@ async function timewarriorCmd() {
|
|
|
875
1031
|
console.log("Timewarrior: enabled");
|
|
876
1032
|
console.log(" Parallel tracking: use `task start <id> --parallel`");
|
|
877
1033
|
}
|
|
878
|
-
const hookExists =
|
|
879
|
-
const hookDisabled =
|
|
1034
|
+
const hookExists = fs4.existsSync(STANDARD_TIMEW_HOOK);
|
|
1035
|
+
const hookDisabled = fs4.existsSync(STANDARD_TIMEW_HOOK + ".disabled");
|
|
880
1036
|
if (hookExists) {
|
|
881
1037
|
console.log(`Standard hook: active (${STANDARD_TIMEW_HOOK})`);
|
|
882
1038
|
if (tw?.enabled) {
|
|
@@ -894,9 +1050,9 @@ async function timewarriorCmd() {
|
|
|
894
1050
|
const configPath = saveConfig(config);
|
|
895
1051
|
console.log("Timewarrior tracking enabled");
|
|
896
1052
|
console.log(`Config: ${configPath}`);
|
|
897
|
-
if (
|
|
1053
|
+
if (fs4.existsSync(STANDARD_TIMEW_HOOK)) {
|
|
898
1054
|
const disabled = STANDARD_TIMEW_HOOK + ".disabled";
|
|
899
|
-
|
|
1055
|
+
fs4.renameSync(STANDARD_TIMEW_HOOK, disabled);
|
|
900
1056
|
console.log(`
|
|
901
1057
|
Disabled standard hook: ${STANDARD_TIMEW_HOOK} -> .disabled`);
|
|
902
1058
|
console.log("tw-bridge will handle Timewarrior tracking directly.");
|
|
@@ -909,8 +1065,8 @@ Disabled standard hook: ${STANDARD_TIMEW_HOOK} -> .disabled`);
|
|
|
909
1065
|
console.log("Timewarrior tracking disabled");
|
|
910
1066
|
console.log(`Config: ${configPath}`);
|
|
911
1067
|
const disabled = STANDARD_TIMEW_HOOK + ".disabled";
|
|
912
|
-
if (
|
|
913
|
-
|
|
1068
|
+
if (fs4.existsSync(disabled)) {
|
|
1069
|
+
fs4.renameSync(disabled, STANDARD_TIMEW_HOOK);
|
|
914
1070
|
console.log(`
|
|
915
1071
|
Restored standard hook: ${STANDARD_TIMEW_HOOK}`);
|
|
916
1072
|
}
|
|
@@ -940,28 +1096,28 @@ task() {
|
|
|
940
1096
|
var SHELL_MARKER = "# tw-bridge: auto-context task wrapper";
|
|
941
1097
|
function installShellFunction() {
|
|
942
1098
|
const shell = process.env.SHELL ?? "/bin/bash";
|
|
943
|
-
const home =
|
|
1099
|
+
const home = os4.homedir();
|
|
944
1100
|
let rcFile;
|
|
945
1101
|
if (shell.endsWith("zsh")) {
|
|
946
|
-
rcFile =
|
|
1102
|
+
rcFile = path5.join(home, ".zshrc");
|
|
947
1103
|
} else {
|
|
948
|
-
rcFile =
|
|
1104
|
+
rcFile = path5.join(home, ".bashrc");
|
|
949
1105
|
}
|
|
950
|
-
const existing =
|
|
1106
|
+
const existing = fs4.existsSync(rcFile) ? fs4.readFileSync(rcFile, "utf-8") : "";
|
|
951
1107
|
if (existing.includes(SHELL_MARKER)) {
|
|
952
1108
|
console.log(`
|
|
953
1109
|
Shell integration already installed in ${rcFile}`);
|
|
954
1110
|
return;
|
|
955
1111
|
}
|
|
956
|
-
|
|
1112
|
+
fs4.appendFileSync(rcFile, "\n" + SHELL_FUNCTION + "\n");
|
|
957
1113
|
console.log(`
|
|
958
1114
|
Shell integration installed in ${rcFile}`);
|
|
959
1115
|
console.log("Restart your shell or run: source " + rcFile);
|
|
960
1116
|
}
|
|
961
|
-
var SEEN_FILE =
|
|
1117
|
+
var SEEN_FILE = path5.join(os4.homedir(), ".config", "tw-bridge", ".seen-dirs");
|
|
962
1118
|
function loadSeenDirs() {
|
|
963
1119
|
try {
|
|
964
|
-
const raw =
|
|
1120
|
+
const raw = fs4.readFileSync(SEEN_FILE, "utf-8");
|
|
965
1121
|
return new Set(raw.split("\n").filter(Boolean));
|
|
966
1122
|
} catch {
|
|
967
1123
|
return /* @__PURE__ */ new Set();
|
|
@@ -971,15 +1127,15 @@ function markDirSeen(dir) {
|
|
|
971
1127
|
const seen = loadSeenDirs();
|
|
972
1128
|
if (seen.has(dir)) return;
|
|
973
1129
|
seen.add(dir);
|
|
974
|
-
|
|
975
|
-
|
|
1130
|
+
fs4.mkdirSync(path5.dirname(SEEN_FILE), { recursive: true });
|
|
1131
|
+
fs4.writeFileSync(SEEN_FILE, [...seen].join("\n") + "\n");
|
|
976
1132
|
}
|
|
977
1133
|
function isGitRepo(dir) {
|
|
978
1134
|
try {
|
|
979
1135
|
let current = dir;
|
|
980
|
-
while (current !==
|
|
981
|
-
if (
|
|
982
|
-
current =
|
|
1136
|
+
while (current !== path5.dirname(current)) {
|
|
1137
|
+
if (fs4.existsSync(path5.join(current, ".git"))) return true;
|
|
1138
|
+
current = path5.dirname(current);
|
|
983
1139
|
}
|
|
984
1140
|
return false;
|
|
985
1141
|
} catch {
|
|
@@ -992,8 +1148,8 @@ async function which() {
|
|
|
992
1148
|
for (const [_name, backend] of Object.entries(config.backends)) {
|
|
993
1149
|
const backendCwd = backend.config?.cwd;
|
|
994
1150
|
if (!backendCwd) continue;
|
|
995
|
-
const resolved =
|
|
996
|
-
if (cwd === resolved || cwd.startsWith(resolved +
|
|
1151
|
+
const resolved = path5.resolve(backendCwd);
|
|
1152
|
+
if (cwd === resolved || cwd.startsWith(resolved + path5.sep)) {
|
|
997
1153
|
const contextTag = backend.match.tags?.[0];
|
|
998
1154
|
if (contextTag) {
|
|
999
1155
|
process.stdout.write(contextTag);
|
|
@@ -1009,15 +1165,15 @@ async function which() {
|
|
|
1009
1165
|
const seen = loadSeenDirs();
|
|
1010
1166
|
let gitRoot = cwd;
|
|
1011
1167
|
let current = cwd;
|
|
1012
|
-
while (current !==
|
|
1013
|
-
if (
|
|
1168
|
+
while (current !== path5.dirname(current)) {
|
|
1169
|
+
if (fs4.existsSync(path5.join(current, ".git"))) {
|
|
1014
1170
|
gitRoot = current;
|
|
1015
1171
|
break;
|
|
1016
1172
|
}
|
|
1017
|
-
current =
|
|
1173
|
+
current = path5.dirname(current);
|
|
1018
1174
|
}
|
|
1019
1175
|
if (!seen.has(gitRoot)) {
|
|
1020
|
-
const dirName =
|
|
1176
|
+
const dirName = path5.basename(gitRoot);
|
|
1021
1177
|
process.stderr.write(
|
|
1022
1178
|
`tw-bridge: unconfigured project "${dirName}". Run: tw-bridge add ${dirName} --adapter ghp
|
|
1023
1179
|
`
|
package/dist/hooks/on-modify.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/hooks/on-modify.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import os3 from "os";
|
|
7
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
4
|
+
import fs4 from "fs";
|
|
5
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
8
6
|
|
|
9
7
|
// src/config.ts
|
|
10
8
|
import fs from "fs";
|
|
@@ -601,7 +599,11 @@ async function resolveAdapter(task, config) {
|
|
|
601
599
|
return adapter;
|
|
602
600
|
}
|
|
603
601
|
|
|
604
|
-
// src/
|
|
602
|
+
// src/tracking.ts
|
|
603
|
+
import fs3 from "fs";
|
|
604
|
+
import path4 from "path";
|
|
605
|
+
import os3 from "os";
|
|
606
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
605
607
|
var TRACKING_FILE = path4.join(os3.homedir(), ".config", "tw-bridge", "tracking.json");
|
|
606
608
|
function loadTracking() {
|
|
607
609
|
try {
|
|
@@ -621,19 +623,6 @@ function mergedTags(state) {
|
|
|
621
623
|
}
|
|
622
624
|
return [...all];
|
|
623
625
|
}
|
|
624
|
-
function timewTags(task) {
|
|
625
|
-
const tags = [];
|
|
626
|
-
if (task.backend) tags.push(task.backend);
|
|
627
|
-
if (task.backend_id) tags.push(`#${task.backend_id}`);
|
|
628
|
-
if (task.project) tags.push(task.project);
|
|
629
|
-
for (const tag of task.tags ?? []) {
|
|
630
|
-
if (!tag.includes("_")) tags.push(tag);
|
|
631
|
-
}
|
|
632
|
-
return [...new Set(tags)];
|
|
633
|
-
}
|
|
634
|
-
function taskKey(task) {
|
|
635
|
-
return task.backend_id ? `#${task.backend_id}` : task.uuid;
|
|
636
|
-
}
|
|
637
626
|
function getCurrentInterval() {
|
|
638
627
|
const result = spawnSync2("timew", ["export"], {
|
|
639
628
|
encoding: "utf-8",
|
|
@@ -647,6 +636,21 @@ function getCurrentInterval() {
|
|
|
647
636
|
return null;
|
|
648
637
|
}
|
|
649
638
|
}
|
|
639
|
+
|
|
640
|
+
// src/hooks/on-modify.ts
|
|
641
|
+
function timewTags(task) {
|
|
642
|
+
const tags = [];
|
|
643
|
+
if (task.backend) tags.push(task.backend);
|
|
644
|
+
if (task.backend_id) tags.push(`#${task.backend_id}`);
|
|
645
|
+
if (task.project) tags.push(task.project);
|
|
646
|
+
for (const tag of task.tags ?? []) {
|
|
647
|
+
if (!tag.includes("_")) tags.push(tag);
|
|
648
|
+
}
|
|
649
|
+
return [...new Set(tags)];
|
|
650
|
+
}
|
|
651
|
+
function taskKey(task) {
|
|
652
|
+
return task.backend_id ? `#${task.backend_id}` : task.uuid;
|
|
653
|
+
}
|
|
650
654
|
function formatDuration(isoStart) {
|
|
651
655
|
const start = new Date(
|
|
652
656
|
isoStart.replace(
|
|
@@ -685,10 +689,10 @@ tw-bridge: Currently tracking: ${tags} (${duration})`
|
|
|
685
689
|
lines.push(" [s] Switch \u2014 stop current, start new task (default)");
|
|
686
690
|
lines.push(" [p] Parallel \u2014 add new task to current tracking");
|
|
687
691
|
lines.push("");
|
|
688
|
-
|
|
689
|
-
|
|
692
|
+
fs4.writeSync(ttyFd, lines.join("\n"));
|
|
693
|
+
fs4.writeSync(ttyFd, " > ");
|
|
690
694
|
const buf = Buffer.alloc(64);
|
|
691
|
-
const bytesRead =
|
|
695
|
+
const bytesRead = fs4.readSync(ttyFd, buf, 0, 64, null);
|
|
692
696
|
const answer = buf.toString("utf-8", 0, bytesRead).trim().toLowerCase();
|
|
693
697
|
return answer.startsWith("p") ? "parallel" : "switch";
|
|
694
698
|
}
|
|
@@ -713,12 +717,12 @@ function handleTimewarriorStart(config, newTask, ttyFd) {
|
|
|
713
717
|
tracking[key] = newTags;
|
|
714
718
|
saveTracking(tracking);
|
|
715
719
|
const merged = mergedTags(tracking);
|
|
716
|
-
|
|
717
|
-
|
|
720
|
+
spawnSync3("timew", ["stop"], { stdio: "pipe" });
|
|
721
|
+
spawnSync3("timew", ["start", ...merged], { stdio: "pipe" });
|
|
718
722
|
} else {
|
|
719
723
|
saveTracking({ [key]: newTags });
|
|
720
|
-
|
|
721
|
-
|
|
724
|
+
spawnSync3("timew", ["stop"], { stdio: "pipe" });
|
|
725
|
+
spawnSync3("timew", ["start", ...newTags], { stdio: "pipe" });
|
|
722
726
|
}
|
|
723
727
|
}
|
|
724
728
|
function handleTimewarriorStop(config, task) {
|
|
@@ -728,13 +732,13 @@ function handleTimewarriorStop(config, task) {
|
|
|
728
732
|
delete tracking[key];
|
|
729
733
|
saveTracking(tracking);
|
|
730
734
|
const remaining = mergedTags(tracking);
|
|
731
|
-
|
|
735
|
+
spawnSync3("timew", ["stop"], { stdio: "pipe" });
|
|
732
736
|
if (remaining.length > 0) {
|
|
733
|
-
|
|
737
|
+
spawnSync3("timew", ["start", ...remaining], { stdio: "pipe" });
|
|
734
738
|
}
|
|
735
739
|
}
|
|
736
740
|
async function main() {
|
|
737
|
-
const input =
|
|
741
|
+
const input = fs4.readFileSync("/dev/stdin", "utf-8").trim().split("\n");
|
|
738
742
|
const oldTask = JSON.parse(input[0]);
|
|
739
743
|
const newTask = JSON.parse(input[1]);
|
|
740
744
|
process.stdout.write(JSON.stringify(newTask) + "\n");
|
|
@@ -745,7 +749,7 @@ async function main() {
|
|
|
745
749
|
let ttyFd = null;
|
|
746
750
|
if (wasStarted) {
|
|
747
751
|
try {
|
|
748
|
-
ttyFd =
|
|
752
|
+
ttyFd = fs4.openSync("/dev/tty", "r+");
|
|
749
753
|
} catch {
|
|
750
754
|
}
|
|
751
755
|
}
|
|
@@ -773,7 +777,7 @@ async function main() {
|
|
|
773
777
|
process.stderr.write(`tw-bridge: ${msg}
|
|
774
778
|
`);
|
|
775
779
|
} finally {
|
|
776
|
-
if (ttyFd !== null)
|
|
780
|
+
if (ttyFd !== null) fs4.closeSync(ttyFd);
|
|
777
781
|
}
|
|
778
782
|
}
|
|
779
783
|
main();
|