@fureworks/scope 0.1.0 → 0.3.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/README.md +129 -48
- package/dist/cli/clean.d.ts +7 -0
- package/dist/cli/clean.d.ts.map +1 -0
- package/dist/cli/clean.js +49 -0
- package/dist/cli/clean.js.map +1 -0
- package/dist/cli/config.d.ts +8 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +368 -42
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/diff.d.ts +6 -0
- package/dist/cli/diff.d.ts.map +1 -0
- package/dist/cli/diff.js +63 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +15 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/notifications.d.ts +7 -0
- package/dist/cli/notifications.d.ts.map +1 -0
- package/dist/cli/notifications.js +77 -0
- package/dist/cli/notifications.js.map +1 -0
- package/dist/cli/onboard.d.ts.map +1 -1
- package/dist/cli/onboard.js +2 -1
- package/dist/cli/onboard.js.map +1 -1
- package/dist/cli/plan.d.ts +7 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +111 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/cli/review.d.ts +6 -0
- package/dist/cli/review.d.ts.map +1 -0
- package/dist/cli/review.js +167 -0
- package/dist/cli/review.js.map +1 -0
- package/dist/cli/snooze.d.ts +12 -0
- package/dist/cli/snooze.d.ts.map +1 -0
- package/dist/cli/snooze.js +155 -0
- package/dist/cli/snooze.js.map +1 -0
- package/dist/cli/today.d.ts.map +1 -1
- package/dist/cli/today.js +85 -11
- package/dist/cli/today.js.map +1 -1
- package/dist/cli/tune.d.ts +8 -0
- package/dist/cli/tune.d.ts.map +1 -0
- package/dist/cli/tune.js +62 -0
- package/dist/cli/tune.js.map +1 -0
- package/dist/engine/__tests__/prioritize.test.d.ts +2 -0
- package/dist/engine/__tests__/prioritize.test.d.ts.map +1 -0
- package/dist/engine/__tests__/prioritize.test.js +199 -0
- package/dist/engine/__tests__/prioritize.test.js.map +1 -0
- package/dist/engine/prioritize.d.ts +10 -2
- package/dist/engine/prioritize.d.ts.map +1 -1
- package/dist/engine/prioritize.js +249 -50
- package/dist/engine/prioritize.js.map +1 -1
- package/dist/index.js +61 -5
- package/dist/index.js.map +1 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +6 -10
- package/dist/notifications/index.js.map +1 -1
- package/dist/sources/__tests__/git.test.d.ts +2 -0
- package/dist/sources/__tests__/git.test.d.ts.map +1 -0
- package/dist/sources/__tests__/git.test.js +52 -0
- package/dist/sources/__tests__/git.test.js.map +1 -0
- package/dist/sources/activity.d.ts +32 -0
- package/dist/sources/activity.d.ts.map +1 -0
- package/dist/sources/activity.js +101 -0
- package/dist/sources/activity.js.map +1 -0
- package/dist/sources/calendar.d.ts +6 -0
- package/dist/sources/calendar.d.ts.map +1 -1
- package/dist/sources/calendar.js +114 -0
- package/dist/sources/calendar.js.map +1 -1
- package/dist/sources/git.d.ts +3 -0
- package/dist/sources/git.d.ts.map +1 -1
- package/dist/sources/git.js +62 -6
- package/dist/sources/git.js.map +1 -1
- package/dist/sources/issues.d.ts +2 -0
- package/dist/sources/issues.d.ts.map +1 -1
- package/dist/sources/issues.js +33 -0
- package/dist/sources/issues.js.map +1 -1
- package/dist/store/config.d.ts +8 -0
- package/dist/store/config.d.ts.map +1 -1
- package/dist/store/config.js +22 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/muted.d.ts +17 -0
- package/dist/store/muted.d.ts.map +1 -0
- package/dist/store/muted.js +55 -0
- package/dist/store/muted.js.map +1 -0
- package/dist/store/snapshot.d.ts +12 -0
- package/dist/store/snapshot.d.ts.map +1 -0
- package/dist/store/snapshot.js +41 -0
- package/dist/store/snapshot.js.map +1 -0
- package/package.json +11 -3
- package/src/cli/config.ts +0 -66
- package/src/cli/context.ts +0 -109
- package/src/cli/daemon.ts +0 -217
- package/src/cli/onboard.ts +0 -335
- package/src/cli/status.ts +0 -77
- package/src/cli/switch.ts +0 -93
- package/src/cli/today.ts +0 -114
- package/src/engine/prioritize.ts +0 -257
- package/src/index.ts +0 -58
- package/src/notifications/index.ts +0 -42
- package/src/sources/calendar.ts +0 -170
- package/src/sources/git.ts +0 -168
- package/src/sources/issues.ts +0 -62
- package/src/store/config.ts +0 -104
- package/tsconfig.json +0 -19
package/dist/store/config.js
CHANGED
|
@@ -2,6 +2,12 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { parse as parseToml } from "toml";
|
|
5
|
+
export const DEFAULT_WEIGHTS = {
|
|
6
|
+
staleness: 1.0,
|
|
7
|
+
blocking: 1.0,
|
|
8
|
+
timePressure: 1.0,
|
|
9
|
+
effort: 1.0,
|
|
10
|
+
};
|
|
5
11
|
const SCOPE_DIR = join(homedir(), ".scope");
|
|
6
12
|
const CONFIG_PATH = join(SCOPE_DIR, "config.toml");
|
|
7
13
|
export function getScopeDir() {
|
|
@@ -26,10 +32,12 @@ export function loadConfig() {
|
|
|
26
32
|
projects: {},
|
|
27
33
|
calendar: { enabled: false, backend: "gws" },
|
|
28
34
|
daemon: { enabled: false, intervalMinutes: 15 },
|
|
35
|
+
weights: { ...DEFAULT_WEIGHTS },
|
|
29
36
|
};
|
|
30
37
|
}
|
|
31
38
|
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
32
39
|
const parsed = parseToml(raw);
|
|
40
|
+
const parsedWeights = parsed.weights;
|
|
33
41
|
return {
|
|
34
42
|
repos: parsed.repos ?? [],
|
|
35
43
|
projects: parsed.projects ?? {},
|
|
@@ -41,6 +49,12 @@ export function loadConfig() {
|
|
|
41
49
|
enabled: parsed.daemon?.enabled ?? false,
|
|
42
50
|
intervalMinutes: parsed.daemon?.intervalMinutes ?? 15,
|
|
43
51
|
},
|
|
52
|
+
weights: {
|
|
53
|
+
staleness: parsedWeights?.staleness ?? DEFAULT_WEIGHTS.staleness,
|
|
54
|
+
blocking: parsedWeights?.blocking ?? DEFAULT_WEIGHTS.blocking,
|
|
55
|
+
timePressure: parsedWeights?.timePressure ?? DEFAULT_WEIGHTS.timePressure,
|
|
56
|
+
effort: parsedWeights?.effort ?? DEFAULT_WEIGHTS.effort,
|
|
57
|
+
},
|
|
44
58
|
};
|
|
45
59
|
}
|
|
46
60
|
export function saveConfig(config) {
|
|
@@ -58,6 +72,14 @@ export function saveConfig(config) {
|
|
|
58
72
|
lines.push(`enabled = ${config.daemon.enabled}`);
|
|
59
73
|
lines.push(`intervalMinutes = ${config.daemon.intervalMinutes}`);
|
|
60
74
|
lines.push("");
|
|
75
|
+
if (config.weights) {
|
|
76
|
+
lines.push("[weights]");
|
|
77
|
+
lines.push(`staleness = ${config.weights.staleness}`);
|
|
78
|
+
lines.push(`blocking = ${config.weights.blocking}`);
|
|
79
|
+
lines.push(`timePressure = ${config.weights.timePressure}`);
|
|
80
|
+
lines.push(`effort = ${config.weights.effort}`);
|
|
81
|
+
lines.push("");
|
|
82
|
+
}
|
|
61
83
|
for (const [name, project] of Object.entries(config.projects)) {
|
|
62
84
|
lines.push(`[projects.${name}]`);
|
|
63
85
|
lines.push(`path = "${project.path}"`);
|
package/dist/store/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/store/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/store/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SnoozeEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
until: string;
|
|
4
|
+
created: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MuteEntry {
|
|
7
|
+
id: string;
|
|
8
|
+
created: string;
|
|
9
|
+
}
|
|
10
|
+
export interface MutedStore {
|
|
11
|
+
snoozed: SnoozeEntry[];
|
|
12
|
+
muted: MuteEntry[];
|
|
13
|
+
}
|
|
14
|
+
export declare function loadMuted(): MutedStore;
|
|
15
|
+
export declare function saveMuted(store: MutedStore): void;
|
|
16
|
+
export declare function isItemMuted(id: string): boolean;
|
|
17
|
+
//# sourceMappingURL=muted.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"muted.d.ts","sourceRoot":"","sources":["../../src/store/muted.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AA+BD,wBAAgB,SAAS,IAAI,UAAU,CAmBtC;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAGjD;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAM/C"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ensureScopeDir, getScopeDir } from "./config.js";
|
|
4
|
+
const MUTED_PATH = join(getScopeDir(), "muted.json");
|
|
5
|
+
function emptyStore() {
|
|
6
|
+
return { snoozed: [], muted: [] };
|
|
7
|
+
}
|
|
8
|
+
function normalizeStore(raw) {
|
|
9
|
+
if (!raw || typeof raw !== "object") {
|
|
10
|
+
return emptyStore();
|
|
11
|
+
}
|
|
12
|
+
const maybeStore = raw;
|
|
13
|
+
return {
|
|
14
|
+
snoozed: Array.isArray(maybeStore.snoozed) ? maybeStore.snoozed : [],
|
|
15
|
+
muted: Array.isArray(maybeStore.muted) ? maybeStore.muted : [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function cleanExpiredSnoozes(store) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
return {
|
|
21
|
+
muted: store.muted,
|
|
22
|
+
snoozed: store.snoozed.filter((entry) => {
|
|
23
|
+
const untilMs = new Date(entry.until).getTime();
|
|
24
|
+
return Number.isFinite(untilMs) && untilMs > now;
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function loadMuted() {
|
|
29
|
+
ensureScopeDir();
|
|
30
|
+
if (!existsSync(MUTED_PATH)) {
|
|
31
|
+
return emptyStore();
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(readFileSync(MUTED_PATH, "utf-8"));
|
|
35
|
+
const normalized = normalizeStore(raw);
|
|
36
|
+
const cleaned = cleanExpiredSnoozes(normalized);
|
|
37
|
+
if (cleaned.snoozed.length !== normalized.snoozed.length) {
|
|
38
|
+
saveMuted(cleaned);
|
|
39
|
+
}
|
|
40
|
+
return cleaned;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return emptyStore();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function saveMuted(store) {
|
|
47
|
+
ensureScopeDir();
|
|
48
|
+
writeFileSync(MUTED_PATH, `${JSON.stringify(store, null, 2)}\n`, "utf-8");
|
|
49
|
+
}
|
|
50
|
+
export function isItemMuted(id) {
|
|
51
|
+
const store = loadMuted();
|
|
52
|
+
return (store.muted.some((entry) => entry.id === id) ||
|
|
53
|
+
store.snoozed.some((entry) => entry.id === id));
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=muted.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"muted.js","sourceRoot":"","sources":["../../src/store/muted.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ScoredItem } from "../engine/prioritize.js";
|
|
2
|
+
export interface DaySnapshot {
|
|
3
|
+
date: string;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
now: ScoredItem[];
|
|
6
|
+
today: ScoredItem[];
|
|
7
|
+
}
|
|
8
|
+
export type TimeContext = "morning" | "midday" | "afternoon" | "evening";
|
|
9
|
+
export declare function getTimeContext(date?: Date): TimeContext;
|
|
10
|
+
export declare function saveSnapshot(now: ScoredItem[], today: ScoredItem[]): void;
|
|
11
|
+
export declare function loadSnapshot(): DaySnapshot | null;
|
|
12
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/store/snapshot.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AASrD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,UAAU,EAAE,CAAC;IAClB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;AAEzE,wBAAgB,cAAc,CAAC,IAAI,GAAE,IAAiB,GAAG,WAAW,CAMnE;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI,CAUzE;AAED,wBAAgB,YAAY,IAAI,WAAW,GAAG,IAAI,CAQjD"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const SNAPSHOTS_DIR = join(homedir(), ".scope", "snapshots");
|
|
5
|
+
function todayKey() {
|
|
6
|
+
const d = new Date();
|
|
7
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
8
|
+
}
|
|
9
|
+
export function getTimeContext(date = new Date()) {
|
|
10
|
+
const hour = date.getHours();
|
|
11
|
+
if (hour < 12)
|
|
12
|
+
return "morning";
|
|
13
|
+
if (hour < 14)
|
|
14
|
+
return "midday";
|
|
15
|
+
if (hour <= 17)
|
|
16
|
+
return "afternoon";
|
|
17
|
+
return "evening";
|
|
18
|
+
}
|
|
19
|
+
export function saveSnapshot(now, today) {
|
|
20
|
+
mkdirSync(SNAPSHOTS_DIR, { recursive: true });
|
|
21
|
+
const snapshot = {
|
|
22
|
+
date: todayKey(),
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
now,
|
|
25
|
+
today,
|
|
26
|
+
};
|
|
27
|
+
const file = join(SNAPSHOTS_DIR, `${todayKey()}.json`);
|
|
28
|
+
writeFileSync(file, JSON.stringify(snapshot, null, 2));
|
|
29
|
+
}
|
|
30
|
+
export function loadSnapshot() {
|
|
31
|
+
const file = join(SNAPSHOTS_DIR, `${todayKey()}.json`);
|
|
32
|
+
if (!existsSync(file))
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/store/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGlC,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;AAE7D,SAAS,QAAQ;IACf,MAAM,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC;IACrB,OAAO,GAAG,CAAC,CAAC,WAAW,EAAE,IAAI,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACnH,CAAC;AAWD,MAAM,UAAU,cAAc,CAAC,OAAa,IAAI,IAAI,EAAE;IACpD,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;IAC7B,IAAI,IAAI,GAAG,EAAE;QAAE,OAAO,SAAS,CAAC;IAChC,IAAI,IAAI,GAAG,EAAE;QAAE,OAAO,QAAQ,CAAC;IAC/B,IAAI,IAAI,IAAI,EAAE;QAAE,OAAO,WAAW,CAAC;IACnC,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAiB,EAAE,KAAmB;IACjE,SAAS,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAgB;QAC5B,IAAI,EAAE,QAAQ,EAAE;QAChB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,GAAG;QACH,KAAK;KACN,CAAC;IACF,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,GAAG,QAAQ,EAAE,OAAO,CAAC,CAAC;IACvD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,GAAG,QAAQ,EAAE,OAAO,CAAC,CAAC;IACvD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAgB,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fureworks/scope",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Personal ops CLI — focus on what matters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"dev": "tsx src/index.ts",
|
|
12
12
|
"lint": "eslint src/",
|
|
13
|
-
"start": "node dist/index.js"
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
15
|
+
"test": "vitest run"
|
|
14
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
15
22
|
"keywords": [
|
|
16
23
|
"cli",
|
|
17
24
|
"productivity",
|
|
@@ -40,6 +47,7 @@
|
|
|
40
47
|
"devDependencies": {
|
|
41
48
|
"@types/node": "^22.0.0",
|
|
42
49
|
"tsx": "^4.19.0",
|
|
43
|
-
"typescript": "^5.7.0"
|
|
50
|
+
"typescript": "^5.7.0",
|
|
51
|
+
"vitest": "^4.1.2"
|
|
44
52
|
}
|
|
45
53
|
}
|
package/src/cli/config.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { loadConfig, configExists } from "../store/config.js";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
|
|
7
|
-
export async function configCommand(
|
|
8
|
-
key?: string,
|
|
9
|
-
value?: string
|
|
10
|
-
): Promise<void> {
|
|
11
|
-
if (!configExists()) {
|
|
12
|
-
console.log(
|
|
13
|
-
chalk.yellow("\n No config found. Run `scope onboard` to get started.\n")
|
|
14
|
-
);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// If no args, show config file contents
|
|
19
|
-
if (!key) {
|
|
20
|
-
const configPath = join(homedir(), ".scope", "config.toml");
|
|
21
|
-
try {
|
|
22
|
-
const content = readFileSync(configPath, "utf-8");
|
|
23
|
-
console.log("");
|
|
24
|
-
console.log(chalk.bold(" ~/.scope/config.toml"));
|
|
25
|
-
console.log(chalk.dim(" ─────────────────────\n"));
|
|
26
|
-
console.log(
|
|
27
|
-
content
|
|
28
|
-
.split("\n")
|
|
29
|
-
.map((line) => ` ${line}`)
|
|
30
|
-
.join("\n")
|
|
31
|
-
);
|
|
32
|
-
console.log("");
|
|
33
|
-
} catch {
|
|
34
|
-
console.log(chalk.yellow("\n Could not read config file.\n"));
|
|
35
|
-
}
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Subcommands
|
|
40
|
-
switch (key) {
|
|
41
|
-
case "git":
|
|
42
|
-
console.log(chalk.dim("\n To manage repos, edit ~/.scope/config.toml"));
|
|
43
|
-
console.log(chalk.dim(" or re-run: scope onboard\n"));
|
|
44
|
-
break;
|
|
45
|
-
case "calendar":
|
|
46
|
-
console.log(
|
|
47
|
-
chalk.dim("\n To set up calendar, install gws:")
|
|
48
|
-
);
|
|
49
|
-
console.log(
|
|
50
|
-
chalk.dim(" npm install -g @googleworkspace/cli")
|
|
51
|
-
);
|
|
52
|
-
console.log(chalk.dim(" Then re-run: scope onboard\n"));
|
|
53
|
-
break;
|
|
54
|
-
case "projects":
|
|
55
|
-
const config = loadConfig();
|
|
56
|
-
console.log(chalk.bold("\n Projects:"));
|
|
57
|
-
for (const [name, project] of Object.entries(config.projects)) {
|
|
58
|
-
console.log(` ${name} → ${project.path}`);
|
|
59
|
-
}
|
|
60
|
-
console.log(chalk.dim("\n Edit: ~/.scope/config.toml\n"));
|
|
61
|
-
break;
|
|
62
|
-
default:
|
|
63
|
-
console.log(chalk.yellow(`\n Unknown config key: ${key}`));
|
|
64
|
-
console.log(chalk.dim(" Available: git, calendar, projects\n"));
|
|
65
|
-
}
|
|
66
|
-
}
|
package/src/cli/context.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { getScopeDir } from "../store/config.js";
|
|
5
|
-
|
|
6
|
-
interface ContextOptions {
|
|
7
|
-
edit?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function contextCommand(options: ContextOptions): Promise<void> {
|
|
11
|
-
const contextsDir = join(getScopeDir(), "contexts");
|
|
12
|
-
|
|
13
|
-
if (!existsSync(contextsDir)) {
|
|
14
|
-
console.log(
|
|
15
|
-
chalk.yellow("\n No project contexts yet. Run `scope switch <project>` first.\n")
|
|
16
|
-
);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Find the most recently switched context
|
|
21
|
-
const files = readdirSync(contextsDir).filter((f) => f.endsWith(".json"));
|
|
22
|
-
|
|
23
|
-
if (files.length === 0) {
|
|
24
|
-
console.log(
|
|
25
|
-
chalk.yellow("\n No project contexts yet. Run `scope switch <project>` first.\n")
|
|
26
|
-
);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let latest: { name: string; data: Record<string, unknown>; time: number } | null = null;
|
|
31
|
-
|
|
32
|
-
for (const file of files) {
|
|
33
|
-
try {
|
|
34
|
-
const data = JSON.parse(
|
|
35
|
-
readFileSync(join(contextsDir, file), "utf-8")
|
|
36
|
-
);
|
|
37
|
-
const time = new Date(data.lastSwitchedAt || 0).getTime();
|
|
38
|
-
if (!latest || time > latest.time) {
|
|
39
|
-
latest = { name: data.name, data, time };
|
|
40
|
-
}
|
|
41
|
-
} catch {
|
|
42
|
-
// Skip corrupt files
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!latest) {
|
|
47
|
-
console.log(
|
|
48
|
-
chalk.yellow("\n No valid contexts found.\n")
|
|
49
|
-
);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const ctx = latest.data as {
|
|
54
|
-
name: string;
|
|
55
|
-
path: string;
|
|
56
|
-
branch: string;
|
|
57
|
-
lastSwitchedAt: string;
|
|
58
|
-
notes: string;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
if (options.edit) {
|
|
62
|
-
const editor = process.env.EDITOR || "vi";
|
|
63
|
-
const { execSync } = await import("node:child_process");
|
|
64
|
-
const notesPath = join(contextsDir, `${ctx.name}.md`);
|
|
65
|
-
|
|
66
|
-
// Create notes file if it doesn't exist
|
|
67
|
-
if (!existsSync(notesPath)) {
|
|
68
|
-
const { writeFileSync } = await import("node:fs");
|
|
69
|
-
writeFileSync(notesPath, `# ${ctx.name}\n\n`, "utf-8");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
execSync(`${editor} ${notesPath}`, { stdio: "inherit" });
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
console.log("");
|
|
77
|
-
console.log(chalk.bold(` Current: ${ctx.name}`));
|
|
78
|
-
console.log(chalk.dim(` ─────────────────────`));
|
|
79
|
-
console.log(` 📁 ${ctx.path}`);
|
|
80
|
-
console.log(` 🌿 ${ctx.branch}`);
|
|
81
|
-
|
|
82
|
-
if (ctx.lastSwitchedAt) {
|
|
83
|
-
const ago = Math.round(
|
|
84
|
-
(Date.now() - new Date(ctx.lastSwitchedAt).getTime()) / (1000 * 60 * 60)
|
|
85
|
-
);
|
|
86
|
-
if (ago < 1) {
|
|
87
|
-
console.log(chalk.dim(` Switched: just now`));
|
|
88
|
-
} else {
|
|
89
|
-
console.log(chalk.dim(` Switched: ${ago}h ago`));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (ctx.notes) {
|
|
94
|
-
console.log(`\n 📝 ${ctx.notes}`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Show other projects
|
|
98
|
-
if (files.length > 1) {
|
|
99
|
-
console.log(chalk.dim(`\n Other projects:`));
|
|
100
|
-
for (const file of files) {
|
|
101
|
-
const name = file.replace(".json", "");
|
|
102
|
-
if (name !== ctx.name) {
|
|
103
|
-
console.log(chalk.dim(` scope switch ${name}`));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
console.log("");
|
|
109
|
-
}
|
package/src/cli/daemon.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { configExists, ensureScopeDir, getScopeDir, loadConfig, saveConfig } from "../store/config.js";
|
|
6
|
-
import { scanAllRepos } from "../sources/git.js";
|
|
7
|
-
import { getCalendarToday } from "../sources/calendar.js";
|
|
8
|
-
import { scanAssignedIssues } from "../sources/issues.js";
|
|
9
|
-
import { prioritize } from "../engine/prioritize.js";
|
|
10
|
-
import { notify } from "../notifications/index.js";
|
|
11
|
-
|
|
12
|
-
const PID_PATH = join(getScopeDir(), "daemon.pid");
|
|
13
|
-
const NOTIFIED_PATH = join(getScopeDir(), "notified.json");
|
|
14
|
-
const DEBOUNCE_MS = 60 * 60 * 1000;
|
|
15
|
-
|
|
16
|
-
type NotifiedState = Record<string, string>;
|
|
17
|
-
|
|
18
|
-
function isProcessRunning(pid: number): boolean {
|
|
19
|
-
try {
|
|
20
|
-
process.kill(pid, 0);
|
|
21
|
-
return true;
|
|
22
|
-
} catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function readPid(): number | null {
|
|
28
|
-
if (!existsSync(PID_PATH)) return null;
|
|
29
|
-
const raw = readFileSync(PID_PATH, "utf-8").trim();
|
|
30
|
-
const pid = Number.parseInt(raw, 10);
|
|
31
|
-
return Number.isFinite(pid) ? pid : null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function removePidFile(): void {
|
|
35
|
-
if (existsSync(PID_PATH)) {
|
|
36
|
-
unlinkSync(PID_PATH);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function loadNotifiedState(): NotifiedState {
|
|
41
|
-
if (!existsSync(NOTIFIED_PATH)) return {};
|
|
42
|
-
try {
|
|
43
|
-
return JSON.parse(readFileSync(NOTIFIED_PATH, "utf-8")) as NotifiedState;
|
|
44
|
-
} catch {
|
|
45
|
-
return {};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function saveNotifiedState(state: NotifiedState): void {
|
|
50
|
-
writeFileSync(NOTIFIED_PATH, JSON.stringify(state, null, 2), "utf-8");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function runSignalCheck(): Promise<void> {
|
|
54
|
-
if (!configExists()) return;
|
|
55
|
-
|
|
56
|
-
const config = loadConfig();
|
|
57
|
-
const gitSignals = await scanAllRepos(config.repos);
|
|
58
|
-
|
|
59
|
-
let calendarEvents: Awaited<ReturnType<typeof getCalendarToday>> = null;
|
|
60
|
-
if (config.calendar.enabled) {
|
|
61
|
-
calendarEvents = await getCalendarToday();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const issueScan = await scanAssignedIssues();
|
|
65
|
-
const events = calendarEvents?.events ?? [];
|
|
66
|
-
const freeBlocks = calendarEvents?.freeBlocks ?? [];
|
|
67
|
-
const result = prioritize(gitSignals, events, freeBlocks, issueScan.issues);
|
|
68
|
-
|
|
69
|
-
const candidates = [...result.now, ...result.today].filter((item) => item.score >= 8);
|
|
70
|
-
if (candidates.length === 0) return;
|
|
71
|
-
|
|
72
|
-
ensureScopeDir();
|
|
73
|
-
const state = loadNotifiedState();
|
|
74
|
-
const nowMs = Date.now();
|
|
75
|
-
let changed = false;
|
|
76
|
-
|
|
77
|
-
for (const item of candidates) {
|
|
78
|
-
const id = `${item.source}:${item.label}`;
|
|
79
|
-
const lastNotified = state[id] ? new Date(state[id]).getTime() : 0;
|
|
80
|
-
if (lastNotified && nowMs - lastNotified < DEBOUNCE_MS) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
notify("Scope: Action Needed", `${item.emoji} ${item.label} — ${item.detail}`);
|
|
85
|
-
state[id] = new Date(nowMs).toISOString();
|
|
86
|
-
changed = true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (changed) {
|
|
90
|
-
saveNotifiedState(state);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function markDaemonEnabled(enabled: boolean): void {
|
|
95
|
-
if (!configExists()) return;
|
|
96
|
-
const config = loadConfig();
|
|
97
|
-
config.daemon.enabled = enabled;
|
|
98
|
-
saveConfig(config);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function daemonCommand(action: string): Promise<void> {
|
|
102
|
-
switch (action) {
|
|
103
|
-
case "start":
|
|
104
|
-
await startDaemon();
|
|
105
|
-
return;
|
|
106
|
-
case "stop":
|
|
107
|
-
stopDaemon();
|
|
108
|
-
return;
|
|
109
|
-
case "status":
|
|
110
|
-
showDaemonStatus();
|
|
111
|
-
return;
|
|
112
|
-
case "run":
|
|
113
|
-
await runDaemonLoop();
|
|
114
|
-
return;
|
|
115
|
-
default:
|
|
116
|
-
console.log(chalk.yellow(`\n Unknown daemon action: ${action}`));
|
|
117
|
-
console.log(chalk.dim(" Use: scope daemon start|stop|status\n"));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function startDaemon(): Promise<void> {
|
|
122
|
-
if (!configExists()) {
|
|
123
|
-
console.log(chalk.yellow("\n Scope isn't set up yet. Run `scope onboard` first.\n"));
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
ensureScopeDir();
|
|
128
|
-
|
|
129
|
-
const existingPid = readPid();
|
|
130
|
-
if (existingPid && isProcessRunning(existingPid)) {
|
|
131
|
-
console.log(chalk.green(`\n Daemon already running (PID ${existingPid}).\n`));
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
removePidFile();
|
|
135
|
-
|
|
136
|
-
const child = spawn(process.execPath, [process.argv[1], "daemon", "run"], {
|
|
137
|
-
detached: true,
|
|
138
|
-
stdio: "ignore",
|
|
139
|
-
});
|
|
140
|
-
child.unref();
|
|
141
|
-
|
|
142
|
-
writeFileSync(PID_PATH, String(child.pid), "utf-8");
|
|
143
|
-
markDaemonEnabled(true);
|
|
144
|
-
|
|
145
|
-
console.log(chalk.green(`\n Daemon started (PID ${child.pid}).\n`));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function stopDaemon(): void {
|
|
149
|
-
const pid = readPid();
|
|
150
|
-
if (!pid) {
|
|
151
|
-
console.log(chalk.dim("\n Daemon is not running.\n"));
|
|
152
|
-
markDaemonEnabled(false);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!isProcessRunning(pid)) {
|
|
157
|
-
removePidFile();
|
|
158
|
-
console.log(chalk.dim("\n Daemon was not running (stale PID removed).\n"));
|
|
159
|
-
markDaemonEnabled(false);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
process.kill(pid);
|
|
165
|
-
removePidFile();
|
|
166
|
-
markDaemonEnabled(false);
|
|
167
|
-
console.log(chalk.green(`\n Daemon stopped (PID ${pid}).\n`));
|
|
168
|
-
} catch {
|
|
169
|
-
console.log(chalk.yellow(`\n Could not stop daemon PID ${pid}.\n`));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function showDaemonStatus(): void {
|
|
174
|
-
const config = loadConfig();
|
|
175
|
-
const pid = readPid();
|
|
176
|
-
|
|
177
|
-
if (pid && isProcessRunning(pid)) {
|
|
178
|
-
console.log(chalk.green(`\n Daemon running (PID ${pid}).`));
|
|
179
|
-
console.log(chalk.dim(` Interval: ${config.daemon.intervalMinutes} minutes\n`));
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (pid) {
|
|
184
|
-
removePidFile();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
console.log(chalk.dim("\n Daemon is not running."));
|
|
188
|
-
console.log(chalk.dim(` Interval: ${config.daemon.intervalMinutes} minutes\n`));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function runDaemonLoop(): Promise<void> {
|
|
192
|
-
ensureScopeDir();
|
|
193
|
-
writeFileSync(PID_PATH, String(process.pid), "utf-8");
|
|
194
|
-
|
|
195
|
-
const cleanup = () => {
|
|
196
|
-
const pid = readPid();
|
|
197
|
-
if (pid === process.pid) {
|
|
198
|
-
removePidFile();
|
|
199
|
-
}
|
|
200
|
-
process.exit(0);
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
process.on("SIGTERM", cleanup);
|
|
204
|
-
process.on("SIGINT", cleanup);
|
|
205
|
-
|
|
206
|
-
while (true) {
|
|
207
|
-
try {
|
|
208
|
-
await runSignalCheck();
|
|
209
|
-
} catch {
|
|
210
|
-
// Keep daemon alive even if checks fail.
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const config = configExists() ? loadConfig() : null;
|
|
214
|
-
const intervalMinutes = Math.max(1, config?.daemon.intervalMinutes ?? 15);
|
|
215
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMinutes * 60 * 1000));
|
|
216
|
-
}
|
|
217
|
-
}
|