@fureworks/scope 0.1.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -0
  3. package/dist/cli/config.d.ts +2 -0
  4. package/dist/cli/config.d.ts.map +1 -0
  5. package/dist/cli/config.js +54 -0
  6. package/dist/cli/config.js.map +1 -0
  7. package/dist/cli/context.d.ts +6 -0
  8. package/dist/cli/context.d.ts.map +1 -0
  9. package/dist/cli/context.js +76 -0
  10. package/dist/cli/context.js.map +1 -0
  11. package/dist/cli/daemon.d.ts +2 -0
  12. package/dist/cli/daemon.d.ts.map +1 -0
  13. package/dist/cli/daemon.js +190 -0
  14. package/dist/cli/daemon.js.map +1 -0
  15. package/dist/cli/onboard.d.ts +2 -0
  16. package/dist/cli/onboard.d.ts.map +1 -0
  17. package/dist/cli/onboard.js +286 -0
  18. package/dist/cli/onboard.js.map +1 -0
  19. package/dist/cli/status.d.ts +6 -0
  20. package/dist/cli/status.d.ts.map +1 -0
  21. package/dist/cli/status.js +57 -0
  22. package/dist/cli/status.js.map +1 -0
  23. package/dist/cli/switch.d.ts +2 -0
  24. package/dist/cli/switch.d.ts.map +1 -0
  25. package/dist/cli/switch.js +78 -0
  26. package/dist/cli/switch.js.map +1 -0
  27. package/dist/cli/today.d.ts +7 -0
  28. package/dist/cli/today.d.ts.map +1 -0
  29. package/dist/cli/today.js +80 -0
  30. package/dist/cli/today.js.map +1 -0
  31. package/dist/engine/prioritize.d.ts +21 -0
  32. package/dist/engine/prioritize.d.ts.map +1 -0
  33. package/dist/engine/prioritize.js +204 -0
  34. package/dist/engine/prioritize.js.map +1 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +48 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/notifications/index.d.ts +2 -0
  40. package/dist/notifications/index.d.ts.map +1 -0
  41. package/dist/notifications/index.js +41 -0
  42. package/dist/notifications/index.js.map +1 -0
  43. package/dist/sources/calendar.d.ts +17 -0
  44. package/dist/sources/calendar.d.ts.map +1 -0
  45. package/dist/sources/calendar.js +120 -0
  46. package/dist/sources/calendar.js.map +1 -0
  47. package/dist/sources/git.d.ts +20 -0
  48. package/dist/sources/git.d.ts.map +1 -0
  49. package/dist/sources/git.js +124 -0
  50. package/dist/sources/git.js.map +1 -0
  51. package/dist/sources/issues.d.ts +14 -0
  52. package/dist/sources/issues.d.ts.map +1 -0
  53. package/dist/sources/issues.js +34 -0
  54. package/dist/sources/issues.js.map +1 -0
  55. package/dist/store/config.d.ts +22 -0
  56. package/dist/store/config.d.ts.map +1 -0
  57. package/dist/store/config.js +74 -0
  58. package/dist/store/config.js.map +1 -0
  59. package/package.json +45 -0
  60. package/src/cli/config.ts +66 -0
  61. package/src/cli/context.ts +109 -0
  62. package/src/cli/daemon.ts +217 -0
  63. package/src/cli/onboard.ts +335 -0
  64. package/src/cli/status.ts +77 -0
  65. package/src/cli/switch.ts +93 -0
  66. package/src/cli/today.ts +114 -0
  67. package/src/engine/prioritize.ts +257 -0
  68. package/src/index.ts +58 -0
  69. package/src/notifications/index.ts +42 -0
  70. package/src/sources/calendar.ts +170 -0
  71. package/src/sources/git.ts +168 -0
  72. package/src/sources/issues.ts +62 -0
  73. package/src/store/config.ts +104 -0
  74. package/tsconfig.json +19 -0
@@ -0,0 +1,335 @@
1
+ import chalk from "chalk";
2
+ import { createInterface } from "node:readline";
3
+ import { execSync } from "node:child_process";
4
+ import { existsSync, readdirSync, statSync } from "node:fs";
5
+ import { resolve, join, basename } from "node:path";
6
+ import { homedir } from "node:os";
7
+ import { ScopeConfig, saveConfig, ensureScopeDir } from "../store/config.js";
8
+
9
+ function ask(rl: ReturnType<typeof createInterface>, question: string): Promise<string> {
10
+ return new Promise((resolve) => {
11
+ rl.question(question, (answer) => {
12
+ resolve(answer.trim());
13
+ });
14
+ });
15
+ }
16
+
17
+ function checkCommand(cmd: string): boolean {
18
+ try {
19
+ execSync(`which ${cmd}`, { stdio: "pipe" });
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ function findGitRepos(): { path: string; name: string }[] {
27
+ const home = homedir();
28
+ const repos: { path: string; name: string }[] = [];
29
+
30
+ // Scan all top-level directories in home for git repos (1-2 levels deep)
31
+ const searchRoots: string[] = [home];
32
+
33
+ // Collect all immediate subdirectories of home as potential search roots
34
+ try {
35
+ const homeEntries = readdirSync(home);
36
+ for (const entry of homeEntries) {
37
+ if (entry.startsWith(".")) continue; // Skip dotfiles/dirs
38
+ const fullPath = join(home, entry);
39
+ try {
40
+ if (statSync(fullPath).isDirectory()) {
41
+ // Check if this dir itself is a repo
42
+ if (existsSync(join(fullPath, ".git"))) {
43
+ repos.push({ path: fullPath, name: entry });
44
+ } else {
45
+ // Search one level deeper inside this directory
46
+ searchRoots.push(fullPath);
47
+ }
48
+ }
49
+ } catch {
50
+ // Skip permission errors
51
+ }
52
+ }
53
+ } catch {
54
+ // Skip if home is inaccessible
55
+ }
56
+
57
+ // Scan each search root (one level deep)
58
+ for (const dir of searchRoots) {
59
+ if (dir === home) continue; // Already scanned top-level
60
+ try {
61
+ const entries = readdirSync(dir);
62
+ for (const entry of entries) {
63
+ if (entry.startsWith(".")) continue;
64
+ const fullPath = join(dir, entry);
65
+ try {
66
+ if (
67
+ statSync(fullPath).isDirectory() &&
68
+ existsSync(join(fullPath, ".git"))
69
+ ) {
70
+ repos.push({ path: fullPath, name: `${basename(dir)}/${entry}` });
71
+ }
72
+ } catch {
73
+ // Skip permission errors
74
+ }
75
+ }
76
+ } catch {
77
+ // Skip inaccessible dirs
78
+ }
79
+ }
80
+
81
+ // Dedupe by path
82
+ const seen = new Set<string>();
83
+ return repos.filter((r) => {
84
+ if (seen.has(r.path)) return false;
85
+ seen.add(r.path);
86
+ return true;
87
+ });
88
+ }
89
+
90
+ function askSelection(
91
+ rl: ReturnType<typeof createInterface>,
92
+ question: string,
93
+ options: { label: string; value: string }[]
94
+ ): Promise<string[]> {
95
+ return new Promise((resolvePromise) => {
96
+ const selected = new Set<number>();
97
+
98
+ console.log(question);
99
+ options.forEach((opt, i) => {
100
+ console.log(chalk.dim(` ${i + 1}) ${opt.label}`));
101
+ });
102
+ console.log(
103
+ chalk.dim(
104
+ `\n Enter numbers separated by commas (e.g. 1,3,5), 'all' for everything, or 'none' to skip`
105
+ )
106
+ );
107
+
108
+ rl.question(" ? Select: ", (answer) => {
109
+ const trimmed = answer.trim().toLowerCase();
110
+
111
+ if (trimmed === "all") {
112
+ resolvePromise(options.map((o) => o.value));
113
+ return;
114
+ }
115
+
116
+ if (trimmed === "none" || trimmed === "") {
117
+ resolvePromise([]);
118
+ return;
119
+ }
120
+
121
+ const indices = trimmed
122
+ .split(",")
123
+ .map((s) => parseInt(s.trim(), 10) - 1)
124
+ .filter((i) => i >= 0 && i < options.length);
125
+
126
+ resolvePromise(indices.map((i) => options[i].value));
127
+ });
128
+ });
129
+ }
130
+
131
+ export async function onboardCommand(): Promise<void> {
132
+ const rl = createInterface({
133
+ input: process.stdin,
134
+ output: process.stdout,
135
+ });
136
+
137
+ console.log("");
138
+ console.log(chalk.bold(" Welcome to Scope — let's get you set up.\n"));
139
+
140
+ const config: ScopeConfig = {
141
+ repos: [],
142
+ projects: {},
143
+ calendar: { enabled: false, backend: "gws" },
144
+ daemon: { enabled: false, intervalMinutes: 15 },
145
+ };
146
+
147
+ // Step 1: Git repos
148
+ console.log(chalk.bold(" Step 1/4: Git repos"));
149
+ console.log(chalk.dim(" ─────────────────────"));
150
+ console.log(chalk.dim(" Scanning for repos...\n"));
151
+
152
+ const foundRepos = findGitRepos();
153
+
154
+ if (foundRepos.length > 0) {
155
+ console.log(
156
+ chalk.green(` Found ${foundRepos.length} repo${foundRepos.length !== 1 ? "s" : ""}:\n`)
157
+ );
158
+
159
+ const selectedPaths = await askSelection(
160
+ rl,
161
+ "",
162
+ foundRepos.map((r) => ({
163
+ label: `${r.name} ${chalk.dim(`(${r.path})`)}`,
164
+ value: r.path,
165
+ }))
166
+ );
167
+
168
+ config.repos.push(...selectedPaths);
169
+
170
+ if (selectedPaths.length > 0) {
171
+ console.log(
172
+ chalk.green(`\n ✓ Added ${selectedPaths.length} repo${selectedPaths.length !== 1 ? "s" : ""}`)
173
+ );
174
+ }
175
+ } else {
176
+ console.log(chalk.dim(" No repos found in common directories."));
177
+ }
178
+
179
+ // Only ask for manual paths if no repos were found automatically
180
+ if (foundRepos.length === 0) {
181
+ console.log("");
182
+ let addingMore = true;
183
+ while (addingMore) {
184
+ const input = await ask(
185
+ rl,
186
+ " ? Add a repo path (or 'done'): "
187
+ );
188
+
189
+ if (input.toLowerCase() === "done" || input === "") {
190
+ addingMore = false;
191
+ } else if (input.startsWith("http://") || input.startsWith("https://") || input.startsWith("git@")) {
192
+ console.log(chalk.yellow(` ✗ Scope needs local paths, not URLs.`));
193
+ console.log(chalk.dim(` Clone it first, then add the local path`));
194
+ } else {
195
+ const resolved = resolve(input.replace(/^~/, process.env.HOME || "~"));
196
+ if (existsSync(resolved)) {
197
+ config.repos.push(resolved);
198
+ console.log(chalk.green(` ✓ Added ${resolved}`));
199
+ } else {
200
+ console.log(chalk.yellow(` ✗ Path not found: ${resolved}`));
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ console.log(
207
+ chalk.green(`\n ✓ Watching ${config.repos.length} repo${config.repos.length !== 1 ? "s" : ""}\n`)
208
+ );
209
+
210
+ // Step 2: GitHub CLI
211
+ console.log(chalk.bold(" Step 2/4: GitHub CLI"));
212
+ console.log(chalk.dim(" ─────────────────────"));
213
+
214
+ const hasGh = checkCommand("gh");
215
+ if (hasGh) {
216
+ console.log(chalk.green(" Checking for gh CLI... ✓ Found"));
217
+ try {
218
+ const authStatus = execSync("gh auth status 2>&1", {
219
+ encoding: "utf-8",
220
+ });
221
+ if (authStatus.includes("Logged in")) {
222
+ console.log(chalk.green(" Checking auth... ✓ Logged in"));
223
+ } else {
224
+ console.log(
225
+ chalk.yellow(" Checking auth... ✗ Not authenticated")
226
+ );
227
+ console.log(chalk.dim(" Run 'gh auth login' to enable PR data\n"));
228
+ }
229
+ } catch {
230
+ console.log(chalk.yellow(" Checking auth... ✗ Not authenticated"));
231
+ console.log(chalk.dim(" Run 'gh auth login' to enable PR data\n"));
232
+ }
233
+ } else {
234
+ console.log(chalk.yellow(" gh CLI not found — PR data will be skipped"));
235
+ console.log(chalk.dim(" Install: https://cli.github.com/\n"));
236
+ }
237
+
238
+ console.log(chalk.green(" ✓ GitHub PR data " + (hasGh ? "available" : "skipped") + "\n"));
239
+
240
+ // Step 3: Google Calendar
241
+ console.log(chalk.bold(" Step 3/4: Google Calendar (optional)"));
242
+ console.log(chalk.dim(" ─────────────────────"));
243
+
244
+ const hasGws = checkCommand("gws");
245
+ if (hasGws) {
246
+ console.log(chalk.green(" Checking for gws CLI... ✓ Found"));
247
+ const enableCal = await ask(
248
+ rl,
249
+ " ? Enable calendar integration? (Y/n): "
250
+ );
251
+ if (enableCal.toLowerCase() !== "n") {
252
+ config.calendar.enabled = true;
253
+ console.log(chalk.green("\n ✓ Calendar enabled\n"));
254
+ } else {
255
+ console.log(chalk.dim("\n Calendar skipped. Enable later with 'scope config calendar'\n"));
256
+ }
257
+ } else {
258
+ console.log(chalk.yellow(" gws CLI not found — calendar will be skipped"));
259
+ console.log(chalk.dim(" Install: npm install -g @googleworkspace/cli"));
260
+ console.log(chalk.dim(" Enable later with 'scope config calendar'\n"));
261
+ }
262
+
263
+ // Step 4: Projects — group repos under names
264
+ console.log(chalk.bold(" Step 4/4: Projects"));
265
+ console.log(chalk.dim(" ─────────────────────"));
266
+ console.log(chalk.dim(" Projects group your repos for context switching."));
267
+ console.log(chalk.dim(" e.g. 'wtl' = your work repos, 'personal' = side projects\n"));
268
+
269
+ if (config.repos.length > 0) {
270
+ let assigningProjects = true;
271
+ const unassigned = [...config.repos];
272
+
273
+ while (assigningProjects && unassigned.length > 0) {
274
+ const projectName = await ask(rl, " ? Project name (or 'done'): ");
275
+
276
+ if (projectName.toLowerCase() === "done" || projectName === "") {
277
+ assigningProjects = false;
278
+ continue;
279
+ }
280
+
281
+ console.log(chalk.dim("\n Which repos belong to this project?\n"));
282
+ unassigned.forEach((r, i) => {
283
+ const name = r.split("/").slice(-2).join("/");
284
+ console.log(chalk.dim(` ${i + 1}) ${name}`));
285
+ });
286
+ console.log(chalk.dim(`\n Enter numbers (e.g. 1,3,5), 'all', or 'none'`));
287
+
288
+ const pick = await ask(rl, " ? Select: ");
289
+ const trimmed = pick.trim().toLowerCase();
290
+
291
+ let selectedPaths: string[] = [];
292
+ if (trimmed === "all") {
293
+ selectedPaths = [...unassigned];
294
+ } else if (trimmed === "none" || trimmed === "") {
295
+ // skip
296
+ } else {
297
+ const indices = trimmed
298
+ .split(",")
299
+ .map((s) => parseInt(s.trim(), 10) - 1)
300
+ .filter((i) => i >= 0 && i < unassigned.length);
301
+ selectedPaths = indices.map((i) => unassigned[i]);
302
+ }
303
+
304
+ if (selectedPaths.length > 0) {
305
+ config.projects[projectName] = { path: selectedPaths[0], repos: selectedPaths };
306
+ // Remove assigned repos from unassigned
307
+ for (const p of selectedPaths) {
308
+ const idx = unassigned.indexOf(p);
309
+ if (idx !== -1) unassigned.splice(idx, 1);
310
+ }
311
+ console.log(
312
+ chalk.green(`\n ✓ Project "${projectName}" — ${selectedPaths.length} repo${selectedPaths.length !== 1 ? "s" : ""}\n`)
313
+ );
314
+ }
315
+
316
+ if (unassigned.length > 0) {
317
+ console.log(chalk.dim(` ${unassigned.length} repo${unassigned.length !== 1 ? "s" : ""} unassigned. Add another project or 'done'.\n`));
318
+ }
319
+ }
320
+ } else {
321
+ console.log(chalk.dim(" No repos to group. Add projects later with 'scope config projects'\n"));
322
+ }
323
+
324
+ // Save
325
+ ensureScopeDir();
326
+ saveConfig(config);
327
+
328
+ console.log(chalk.dim(" ─────────────────────"));
329
+ console.log(chalk.bold.green(" Setup complete!"));
330
+ console.log(chalk.dim(` Config saved to ~/.scope/config.toml\n`));
331
+ console.log(` Try: ${chalk.bold("scope today")}`);
332
+ console.log(chalk.dim(`\n Tip: run 'npm link' in this directory to use 'scope' globally\n`));
333
+
334
+ rl.close();
335
+ }
@@ -0,0 +1,77 @@
1
+ import chalk from "chalk";
2
+ import { loadConfig, configExists } from "../store/config.js";
3
+ import { scanAllRepos } from "../sources/git.js";
4
+
5
+ interface StatusOptions {
6
+ json?: boolean;
7
+ }
8
+
9
+ export async function statusCommand(options: StatusOptions): Promise<void> {
10
+ if (!configExists()) {
11
+ console.log(
12
+ chalk.yellow(" Scope isn't set up yet. Run `scope onboard` to get started.\n")
13
+ );
14
+ return;
15
+ }
16
+
17
+ const config = loadConfig();
18
+ const gitSignals = await scanAllRepos(config.repos);
19
+
20
+ if (options.json) {
21
+ console.log(JSON.stringify({ repos: gitSignals, config }, null, 2));
22
+ return;
23
+ }
24
+
25
+ console.log("");
26
+ console.log(chalk.bold(" Scope Status"));
27
+ console.log(chalk.dim(" ─────────────────────\n"));
28
+
29
+ // Repos
30
+ console.log(chalk.bold(" Repos"));
31
+ if (gitSignals.length === 0) {
32
+ console.log(chalk.dim(" No repos configured or accessible.\n"));
33
+ } else {
34
+ for (const signal of gitSignals) {
35
+ const status: string[] = [];
36
+ if (signal.uncommittedFiles > 0) {
37
+ status.push(
38
+ chalk.yellow(`${signal.uncommittedFiles} uncommitted`)
39
+ );
40
+ }
41
+ if (signal.openPRs.length > 0) {
42
+ status.push(chalk.blue(`${signal.openPRs.length} PRs`));
43
+ }
44
+ if (signal.staleBranches.length > 0) {
45
+ status.push(
46
+ chalk.dim(`${signal.staleBranches.length} stale branches`)
47
+ );
48
+ }
49
+ if (status.length === 0) {
50
+ status.push(chalk.green("clean"));
51
+ }
52
+
53
+ console.log(
54
+ ` ${signal.repo} ${chalk.dim(`(${signal.branch})`)} — ${status.join(", ")}`
55
+ );
56
+ }
57
+ console.log("");
58
+ }
59
+
60
+ // Projects
61
+ const projectNames = Object.keys(config.projects);
62
+ if (projectNames.length > 0) {
63
+ console.log(chalk.bold(" Projects"));
64
+ for (const name of projectNames) {
65
+ const p = config.projects[name];
66
+ console.log(` ${name} ${chalk.dim(`→ ${p.path}`)}`);
67
+ }
68
+ console.log("");
69
+ }
70
+
71
+ // Calendar
72
+ console.log(chalk.bold(" Integrations"));
73
+ console.log(
74
+ ` Calendar: ${config.calendar.enabled ? chalk.green("enabled") : chalk.dim("disabled")}`
75
+ );
76
+ console.log("");
77
+ }
@@ -0,0 +1,93 @@
1
+ import chalk from "chalk";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { loadConfig, getScopeDir } from "../store/config.js";
5
+ import { simpleGit } from "simple-git";
6
+
7
+ interface ProjectContext {
8
+ name: string;
9
+ path: string;
10
+ branch: string;
11
+ lastSwitchedAt: string;
12
+ notes: string;
13
+ }
14
+
15
+ function getContextPath(projectName: string): string {
16
+ return join(getScopeDir(), "contexts", `${projectName}.json`);
17
+ }
18
+
19
+ function loadContext(projectName: string): ProjectContext | null {
20
+ const contextPath = getContextPath(projectName);
21
+ if (!existsSync(contextPath)) return null;
22
+ try {
23
+ return JSON.parse(readFileSync(contextPath, "utf-8"));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function saveContext(context: ProjectContext): void {
30
+ const dir = join(getScopeDir(), "contexts");
31
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
32
+ writeFileSync(getContextPath(context.name), JSON.stringify(context, null, 2));
33
+ }
34
+
35
+ export async function switchCommand(project: string): Promise<void> {
36
+ const config = loadConfig();
37
+
38
+ const projectConfig = config.projects[project];
39
+ if (!projectConfig) {
40
+ console.log(
41
+ chalk.yellow(`\n Project "${project}" not found.\n`)
42
+ );
43
+ console.log(chalk.dim(" Available projects:"));
44
+ for (const name of Object.keys(config.projects)) {
45
+ console.log(chalk.dim(` - ${name}`));
46
+ }
47
+ console.log(chalk.dim(`\n Add with: scope config projects\n`));
48
+ return;
49
+ }
50
+
51
+ // Save current context if we can detect one
52
+ // (We save the project we're switching FROM)
53
+
54
+ // Load target context
55
+ const existingContext = loadContext(project);
56
+
57
+ // Get current git branch for the target project
58
+ let branch = "unknown";
59
+ try {
60
+ const git = simpleGit(projectConfig.path);
61
+ const branchInfo = await git.branch();
62
+ branch = branchInfo.current;
63
+ } catch {
64
+ // Not a git repo or error
65
+ }
66
+
67
+ // Save/update context
68
+ const context: ProjectContext = {
69
+ name: project,
70
+ path: projectConfig.path,
71
+ branch,
72
+ lastSwitchedAt: new Date().toISOString(),
73
+ notes: existingContext?.notes || "",
74
+ };
75
+ saveContext(context);
76
+
77
+ console.log("");
78
+ console.log(chalk.bold(` Switched to: ${project}`));
79
+ console.log(chalk.dim(` ─────────────────────`));
80
+ console.log(` 📁 ${projectConfig.path}`);
81
+ console.log(` 🌿 ${branch}`);
82
+ if (existingContext?.notes) {
83
+ console.log(` 📝 ${existingContext.notes}`);
84
+ }
85
+ if (existingContext?.lastSwitchedAt) {
86
+ const last = new Date(existingContext.lastSwitchedAt);
87
+ const ago = Math.round((Date.now() - last.getTime()) / (1000 * 60 * 60));
88
+ console.log(chalk.dim(` Last here: ${ago}h ago`));
89
+ }
90
+ console.log("");
91
+ console.log(chalk.dim(` cd ${projectConfig.path}`));
92
+ console.log("");
93
+ }
@@ -0,0 +1,114 @@
1
+ import chalk from "chalk";
2
+ import { loadConfig, configExists } from "../store/config.js";
3
+ import { scanAllRepos } from "../sources/git.js";
4
+ import { getCalendarToday } from "../sources/calendar.js";
5
+ import { scanAssignedIssues } from "../sources/issues.js";
6
+ import { prioritize } from "../engine/prioritize.js";
7
+
8
+ interface TodayOptions {
9
+ calendar?: boolean;
10
+ json?: boolean;
11
+ }
12
+
13
+ export async function todayCommand(options: TodayOptions): Promise<void> {
14
+ if (!configExists()) {
15
+ console.log(
16
+ chalk.yellow(
17
+ " Scope isn't set up yet. Run `scope onboard` to get started.\n"
18
+ )
19
+ );
20
+ process.exit(1);
21
+ }
22
+
23
+ const config = loadConfig();
24
+
25
+ if (config.repos.length === 0) {
26
+ console.log(
27
+ chalk.yellow(" No repos configured. Run `scope config git` to add some.\n")
28
+ );
29
+ process.exit(1);
30
+ }
31
+
32
+ // Scan git repos
33
+ const gitSignals = await scanAllRepos(config.repos);
34
+
35
+ // Get calendar events
36
+ let calendarEvents: Awaited<ReturnType<typeof getCalendarToday>> = null;
37
+ if (options.calendar !== false && config.calendar.enabled) {
38
+ calendarEvents = await getCalendarToday();
39
+ if (!calendarEvents) {
40
+ console.log(
41
+ chalk.dim(
42
+ " ⚠ Calendar not available. Try: gws auth login\n"
43
+ )
44
+ );
45
+ }
46
+ }
47
+
48
+ const events = calendarEvents?.events ?? [];
49
+ const freeBlocks = calendarEvents?.freeBlocks ?? [];
50
+ const issueScan = await scanAssignedIssues();
51
+ if (!issueScan.available) {
52
+ console.log(
53
+ chalk.dim(" ⚠ GitHub issues not available. Install/auth gh to enable issue signals.\n")
54
+ );
55
+ }
56
+
57
+ // Prioritize
58
+ const result = prioritize(gitSignals, events, freeBlocks, issueScan.issues);
59
+
60
+ // Output
61
+ if (options.json) {
62
+ console.log(JSON.stringify(result, null, 2));
63
+ return;
64
+ }
65
+
66
+ console.log("");
67
+
68
+ if (result.now.length === 0 && result.today.length === 0) {
69
+ console.log(chalk.green(" ✓ Nothing urgent. You're clear.\n"));
70
+ if (result.laterCount > 0) {
71
+ console.log(
72
+ chalk.dim(` ${result.laterCount} low-priority items → scope status\n`)
73
+ );
74
+ }
75
+ return;
76
+ }
77
+
78
+ // NOW section
79
+ if (result.now.length > 0) {
80
+ console.log(chalk.bold(" NOW"));
81
+ console.log(chalk.dim(" ───"));
82
+ for (const item of result.now) {
83
+ console.log(` ${item.emoji} ${chalk.bold(item.label)}`);
84
+ console.log(` ${chalk.dim(item.detail)}`);
85
+ }
86
+ console.log("");
87
+ }
88
+
89
+ // TODAY section
90
+ if (result.today.length > 0) {
91
+ console.log(chalk.bold(" TODAY"));
92
+ console.log(chalk.dim(" ────"));
93
+ for (const item of result.today) {
94
+ console.log(` ${item.emoji} ${chalk.bold(item.label)}`);
95
+ console.log(` ${chalk.dim(item.detail)}`);
96
+ }
97
+ console.log("");
98
+ }
99
+
100
+ // Suggestions
101
+ if (result.suggestions.length > 0) {
102
+ for (const suggestion of result.suggestions) {
103
+ console.log(` 💡 ${suggestion}`);
104
+ }
105
+ console.log("");
106
+ }
107
+
108
+ // Later count
109
+ if (result.laterCount > 0) {
110
+ console.log(
111
+ chalk.dim(` ${result.laterCount} other items can wait → scope status\n`)
112
+ );
113
+ }
114
+ }