@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.
Files changed (104) hide show
  1. package/README.md +129 -48
  2. package/dist/cli/clean.d.ts +7 -0
  3. package/dist/cli/clean.d.ts.map +1 -0
  4. package/dist/cli/clean.js +49 -0
  5. package/dist/cli/clean.js.map +1 -0
  6. package/dist/cli/config.d.ts +8 -1
  7. package/dist/cli/config.d.ts.map +1 -1
  8. package/dist/cli/config.js +368 -42
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/diff.d.ts +6 -0
  11. package/dist/cli/diff.d.ts.map +1 -0
  12. package/dist/cli/diff.js +63 -0
  13. package/dist/cli/diff.js.map +1 -0
  14. package/dist/cli/init.d.ts +2 -0
  15. package/dist/cli/init.d.ts.map +1 -0
  16. package/dist/cli/init.js +15 -0
  17. package/dist/cli/init.js.map +1 -0
  18. package/dist/cli/notifications.d.ts +7 -0
  19. package/dist/cli/notifications.d.ts.map +1 -0
  20. package/dist/cli/notifications.js +77 -0
  21. package/dist/cli/notifications.js.map +1 -0
  22. package/dist/cli/onboard.d.ts.map +1 -1
  23. package/dist/cli/onboard.js +2 -1
  24. package/dist/cli/onboard.js.map +1 -1
  25. package/dist/cli/plan.d.ts +7 -0
  26. package/dist/cli/plan.d.ts.map +1 -0
  27. package/dist/cli/plan.js +111 -0
  28. package/dist/cli/plan.js.map +1 -0
  29. package/dist/cli/review.d.ts +6 -0
  30. package/dist/cli/review.d.ts.map +1 -0
  31. package/dist/cli/review.js +167 -0
  32. package/dist/cli/review.js.map +1 -0
  33. package/dist/cli/snooze.d.ts +12 -0
  34. package/dist/cli/snooze.d.ts.map +1 -0
  35. package/dist/cli/snooze.js +155 -0
  36. package/dist/cli/snooze.js.map +1 -0
  37. package/dist/cli/today.d.ts.map +1 -1
  38. package/dist/cli/today.js +85 -11
  39. package/dist/cli/today.js.map +1 -1
  40. package/dist/cli/tune.d.ts +8 -0
  41. package/dist/cli/tune.d.ts.map +1 -0
  42. package/dist/cli/tune.js +62 -0
  43. package/dist/cli/tune.js.map +1 -0
  44. package/dist/engine/__tests__/prioritize.test.d.ts +2 -0
  45. package/dist/engine/__tests__/prioritize.test.d.ts.map +1 -0
  46. package/dist/engine/__tests__/prioritize.test.js +199 -0
  47. package/dist/engine/__tests__/prioritize.test.js.map +1 -0
  48. package/dist/engine/prioritize.d.ts +10 -2
  49. package/dist/engine/prioritize.d.ts.map +1 -1
  50. package/dist/engine/prioritize.js +249 -50
  51. package/dist/engine/prioritize.js.map +1 -1
  52. package/dist/index.js +61 -5
  53. package/dist/index.js.map +1 -1
  54. package/dist/notifications/index.d.ts.map +1 -1
  55. package/dist/notifications/index.js +6 -10
  56. package/dist/notifications/index.js.map +1 -1
  57. package/dist/sources/__tests__/git.test.d.ts +2 -0
  58. package/dist/sources/__tests__/git.test.d.ts.map +1 -0
  59. package/dist/sources/__tests__/git.test.js +52 -0
  60. package/dist/sources/__tests__/git.test.js.map +1 -0
  61. package/dist/sources/activity.d.ts +32 -0
  62. package/dist/sources/activity.d.ts.map +1 -0
  63. package/dist/sources/activity.js +101 -0
  64. package/dist/sources/activity.js.map +1 -0
  65. package/dist/sources/calendar.d.ts +6 -0
  66. package/dist/sources/calendar.d.ts.map +1 -1
  67. package/dist/sources/calendar.js +114 -0
  68. package/dist/sources/calendar.js.map +1 -1
  69. package/dist/sources/git.d.ts +3 -0
  70. package/dist/sources/git.d.ts.map +1 -1
  71. package/dist/sources/git.js +62 -6
  72. package/dist/sources/git.js.map +1 -1
  73. package/dist/sources/issues.d.ts +2 -0
  74. package/dist/sources/issues.d.ts.map +1 -1
  75. package/dist/sources/issues.js +33 -0
  76. package/dist/sources/issues.js.map +1 -1
  77. package/dist/store/config.d.ts +8 -0
  78. package/dist/store/config.d.ts.map +1 -1
  79. package/dist/store/config.js +22 -0
  80. package/dist/store/config.js.map +1 -1
  81. package/dist/store/muted.d.ts +17 -0
  82. package/dist/store/muted.d.ts.map +1 -0
  83. package/dist/store/muted.js +55 -0
  84. package/dist/store/muted.js.map +1 -0
  85. package/dist/store/snapshot.d.ts +12 -0
  86. package/dist/store/snapshot.d.ts.map +1 -0
  87. package/dist/store/snapshot.js +41 -0
  88. package/dist/store/snapshot.js.map +1 -0
  89. package/package.json +11 -3
  90. package/src/cli/config.ts +0 -66
  91. package/src/cli/context.ts +0 -109
  92. package/src/cli/daemon.ts +0 -217
  93. package/src/cli/onboard.ts +0 -335
  94. package/src/cli/status.ts +0 -77
  95. package/src/cli/switch.ts +0 -93
  96. package/src/cli/today.ts +0 -114
  97. package/src/engine/prioritize.ts +0 -257
  98. package/src/index.ts +0 -58
  99. package/src/notifications/index.ts +0 -42
  100. package/src/sources/calendar.ts +0 -170
  101. package/src/sources/git.ts +0 -168
  102. package/src/sources/issues.ts +0 -62
  103. package/src/store/config.ts +0 -104
  104. package/tsconfig.json +0 -19
@@ -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}"`);
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/store/config.ts"],"names":[],"mappings":"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"}
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.1.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
- }
@@ -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
- }