@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.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +54 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/context.d.ts +6 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +76 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/daemon.d.ts +2 -0
- package/dist/cli/daemon.d.ts.map +1 -0
- package/dist/cli/daemon.js +190 -0
- package/dist/cli/daemon.js.map +1 -0
- package/dist/cli/onboard.d.ts +2 -0
- package/dist/cli/onboard.d.ts.map +1 -0
- package/dist/cli/onboard.js +286 -0
- package/dist/cli/onboard.js.map +1 -0
- package/dist/cli/status.d.ts +6 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +57 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/switch.d.ts +2 -0
- package/dist/cli/switch.d.ts.map +1 -0
- package/dist/cli/switch.js +78 -0
- package/dist/cli/switch.js.map +1 -0
- package/dist/cli/today.d.ts +7 -0
- package/dist/cli/today.d.ts.map +1 -0
- package/dist/cli/today.js +80 -0
- package/dist/cli/today.js.map +1 -0
- package/dist/engine/prioritize.d.ts +21 -0
- package/dist/engine/prioritize.d.ts.map +1 -0
- package/dist/engine/prioritize.js +204 -0
- package/dist/engine/prioritize.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/notifications/index.d.ts +2 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +41 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/sources/calendar.d.ts +17 -0
- package/dist/sources/calendar.d.ts.map +1 -0
- package/dist/sources/calendar.js +120 -0
- package/dist/sources/calendar.js.map +1 -0
- package/dist/sources/git.d.ts +20 -0
- package/dist/sources/git.d.ts.map +1 -0
- package/dist/sources/git.js +124 -0
- package/dist/sources/git.js.map +1 -0
- package/dist/sources/issues.d.ts +14 -0
- package/dist/sources/issues.d.ts.map +1 -0
- package/dist/sources/issues.js +34 -0
- package/dist/sources/issues.js.map +1 -0
- package/dist/store/config.d.ts +22 -0
- package/dist/store/config.d.ts.map +1 -0
- package/dist/store/config.js +74 -0
- package/dist/store/config.js.map +1 -0
- package/package.json +45 -0
- package/src/cli/config.ts +66 -0
- package/src/cli/context.ts +109 -0
- package/src/cli/daemon.ts +217 -0
- package/src/cli/onboard.ts +335 -0
- package/src/cli/status.ts +77 -0
- package/src/cli/switch.ts +93 -0
- package/src/cli/today.ts +114 -0
- package/src/engine/prioritize.ts +257 -0
- package/src/index.ts +58 -0
- package/src/notifications/index.ts +42 -0
- package/src/sources/calendar.ts +170 -0
- package/src/sources/git.ts +168 -0
- package/src/sources/issues.ts +62 -0
- package/src/store/config.ts +104 -0
- 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
|
+
}
|
package/src/cli/today.ts
ADDED
|
@@ -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
|
+
}
|