@fureworks/scope 0.1.0 → 0.2.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/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/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 +69 -9
- 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/prioritize.d.ts +10 -2
- package/dist/engine/prioritize.d.ts.map +1 -1
- package/dist/engine/prioritize.js +156 -25
- package/dist/engine/prioritize.js.map +1 -1
- package/dist/index.js +48 -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/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/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 +8 -2
- 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/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
|
-
}
|
package/src/cli/onboard.ts
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
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
|
-
}
|
package/src/cli/status.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
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
|
-
}
|