@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/README.md
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
# Scope
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Scope tells you the 3 things that matter right now — and gives you permission to ignore everything else.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Why
|
|
8
|
-
|
|
9
|
-
AI tools made you more capable. Which means more gets piled on. The bottleneck moved from *execution* to *prioritization and context switching*.
|
|
10
|
-
|
|
11
|
-
Scope sits above your tools and helps you focus.
|
|
5
|
+
A personal ops CLI for builders who juggle multiple repos, PRs, meetings, and issues. Scope reads your existing workflow signals and surfaces what needs attention. No manual input. No new accounts. No cloud.
|
|
12
6
|
|
|
13
7
|
## Install
|
|
14
8
|
|
|
@@ -16,77 +10,164 @@ Scope sits above your tools and helps you focus.
|
|
|
16
10
|
npm install -g @fureworks/scope
|
|
17
11
|
```
|
|
18
12
|
|
|
13
|
+
Requires Node.js 18+.
|
|
14
|
+
|
|
19
15
|
## Quick Start
|
|
20
16
|
|
|
21
17
|
```bash
|
|
22
|
-
scope onboard
|
|
23
|
-
scope today
|
|
18
|
+
scope onboard # guided first-time setup
|
|
19
|
+
scope today # what matters right now
|
|
20
|
+
scope review # end-of-day summary
|
|
24
21
|
```
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
Or set up non-interactively (great for AI agents):
|
|
27
24
|
|
|
28
|
-
```
|
|
29
|
-
scope
|
|
30
|
-
scope
|
|
31
|
-
scope
|
|
32
|
-
scope
|
|
33
|
-
scope
|
|
34
|
-
scope review End-of-day summary
|
|
35
|
-
scope config View/edit configuration
|
|
25
|
+
```bash
|
|
26
|
+
scope init
|
|
27
|
+
scope config repos add ~/projects/my-app ~/projects/api
|
|
28
|
+
scope config repos scan ~/work # auto-discover git repos
|
|
29
|
+
scope config calendar enable
|
|
30
|
+
scope config projects add myproject --dir ~/projects/my-app
|
|
36
31
|
```
|
|
37
32
|
|
|
38
|
-
##
|
|
33
|
+
## What It Does
|
|
39
34
|
|
|
40
|
-
Scope reads signals from
|
|
35
|
+
Scope reads signals from tools you already use:
|
|
41
36
|
|
|
42
|
-
- **Git** — uncommitted
|
|
43
|
-
- **
|
|
44
|
-
- **
|
|
37
|
+
- **Git** — uncommitted work, stale branches, recent activity
|
|
38
|
+
- **GitHub** — open PRs, review requests, CI status, assigned issues
|
|
39
|
+
- **Google Calendar** — meetings, free blocks (via `gws` CLI)
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
Then it scores and ranks everything using deterministic rules (no AI):
|
|
47
42
|
|
|
48
43
|
```
|
|
49
44
|
$ scope today
|
|
50
45
|
|
|
46
|
+
Good morning. Here's what matters:
|
|
47
|
+
|
|
51
48
|
NOW
|
|
52
49
|
───
|
|
53
|
-
🔴
|
|
54
|
-
|
|
50
|
+
🔴 PR #8 on api-service (low context: no CI status)
|
|
51
|
+
auth migration — review requested, 3 days old
|
|
52
|
+
Why: Someone's waiting on your review. 3 days stale.
|
|
55
53
|
|
|
56
54
|
TODAY
|
|
57
55
|
────
|
|
58
|
-
🟡 fureworks/scope
|
|
59
|
-
|
|
56
|
+
🟡 fureworks/scope
|
|
57
|
+
3 uncommitted files, last touched 6h ago
|
|
58
|
+
Why: Uncommitted work for 6 hours. Commit or stash.
|
|
59
|
+
|
|
60
|
+
IGNORED
|
|
61
|
+
───────
|
|
62
|
+
✗ PR #2 on docs — Fresh, no one's waiting
|
|
63
|
+
✗ Issue #12 on scope — Less than a week old, no priority label
|
|
64
|
+
|
|
65
|
+
Nothing else needs you today.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Commands
|
|
69
|
+
|
|
70
|
+
| Command | What it does |
|
|
71
|
+
|---------|-------------|
|
|
72
|
+
| `scope today` | Morning priorities — what needs attention right now |
|
|
73
|
+
| `scope review` | End-of-day summary — what got done, what's carrying over |
|
|
74
|
+
| `scope plan` | Weekly view — calendar density, PR backlog, best build days |
|
|
75
|
+
| `scope status` | Overview of all watched projects |
|
|
76
|
+
| `scope switch <project>` | Context switch between projects |
|
|
77
|
+
| `scope context` | Show current project state |
|
|
78
|
+
| `scope snooze <item> --until <date>` | Hide an item until a date |
|
|
79
|
+
| `scope mute <item>` | Permanently hide an item |
|
|
80
|
+
| `scope tune [key] [value]` | Adjust scoring weights |
|
|
81
|
+
| `scope config repos\|calendar\|projects` | Manage configuration |
|
|
82
|
+
| `scope init` | Initialize Scope |
|
|
83
|
+
| `scope onboard` | Interactive guided setup |
|
|
84
|
+
| `scope daemon start\|stop\|status` | Background signal checks |
|
|
85
|
+
| `scope notifications` | View recent alerts |
|
|
86
|
+
|
|
87
|
+
## How Scoring Works
|
|
88
|
+
|
|
89
|
+
Each item gets a priority score based on measurable signals:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
Score = (Time Pressure + Staleness + Blocking Potential + Effort Match) × Weight
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- **Score ≥ 8** → 🔴 **NOW** — do these first (max 3 shown)
|
|
96
|
+
- **Score 4–7** → 🟡 **TODAY** — fit these in (max 5 shown)
|
|
97
|
+
- **Score < 4** → **IGNORED** — shown with reason, explicitly excluded
|
|
60
98
|
|
|
61
|
-
|
|
62
|
-
Good for: fureworks/scope (has pending work)
|
|
99
|
+
Adjust weights with `scope tune`:
|
|
63
100
|
|
|
64
|
-
|
|
101
|
+
```bash
|
|
102
|
+
scope tune staleness 1.5 # stale items rank higher
|
|
103
|
+
scope tune blocking 0.5 # reduce blocking urgency
|
|
104
|
+
scope tune --reset # restore defaults
|
|
65
105
|
```
|
|
66
106
|
|
|
67
|
-
##
|
|
107
|
+
## Time Awareness
|
|
108
|
+
|
|
109
|
+
`scope today` adjusts its tone based on when you run it:
|
|
68
110
|
|
|
69
|
-
- **
|
|
70
|
-
- **
|
|
71
|
-
- **
|
|
72
|
-
|
|
73
|
-
|
|
111
|
+
- **Morning:** "Good morning. Here's what matters."
|
|
112
|
+
- **Afternoon:** "3/5 from this morning done. 2 remaining."
|
|
113
|
+
- **Evening:** "Run `scope review` to wrap up."
|
|
114
|
+
|
|
115
|
+
## Weekly Planning
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
$ scope plan
|
|
119
|
+
|
|
120
|
+
THIS WEEK
|
|
121
|
+
─────────
|
|
122
|
+
Mon ████░░ 3 meetings, 2h free
|
|
123
|
+
Tue ██░░░░ 1 meeting, 5h free ← best deep work day
|
|
124
|
+
Wed █████░ 4 meetings, 1h free
|
|
125
|
+
Thu ███░░░ 2 meetings, 4h free
|
|
126
|
+
Fri █░░░░░ 0 meetings, 7h free
|
|
127
|
+
|
|
128
|
+
BACKLOG
|
|
129
|
+
───────
|
|
130
|
+
3 PRs older than 1 week
|
|
131
|
+
2 issues approaching stale (>14 days)
|
|
132
|
+
|
|
133
|
+
💡 Tuesday + Friday are your best build days this week.
|
|
134
|
+
```
|
|
74
135
|
|
|
75
136
|
## Prerequisites
|
|
76
137
|
|
|
77
|
-
|
|
78
|
-
- Git
|
|
79
|
-
- [GitHub CLI](https://cli.github.com/) (`gh`) — for PR data
|
|
80
|
-
- [Google Workspace CLI](https://github.com/googleworkspace/cli) (`gws`) — for calendar (optional)
|
|
138
|
+
Scope reads from external tools. All are optional — missing integrations reduce output but never crash.
|
|
81
139
|
|
|
82
|
-
|
|
140
|
+
| Tool | What it provides | Install |
|
|
141
|
+
|------|-----------------|---------|
|
|
142
|
+
| `gh` (GitHub CLI) | PRs, issues, CI status | [cli.github.com](https://cli.github.com/) |
|
|
143
|
+
| `gws` (Google Workspace CLI) | Calendar events, free blocks | `npm i -g @googleworkspace/cli` |
|
|
83
144
|
|
|
84
|
-
|
|
145
|
+
## Best Practices
|
|
85
146
|
|
|
86
|
-
|
|
147
|
+
See [WORKFLOW.md](./WORKFLOW.md) for habits that make Scope's output better — commit often, assign yourself, use labels, block focus time.
|
|
148
|
+
|
|
149
|
+
## Philosophy
|
|
150
|
+
|
|
151
|
+
- **Zero manual input.** Scope reads. It doesn't ask you to enter data.
|
|
152
|
+
- **Confident exclusion.** The value isn't what's shown — it's what's hidden and why.
|
|
153
|
+
- **Degraded mode is fine.** Only have git? Scope works. Add calendar? Better. Each integration is additive.
|
|
154
|
+
- **CLI-first, local-first.** No accounts, no cloud, no tracking.
|
|
155
|
+
- **No AI (v1).** Deterministic rules-based scoring. Transparent and predictable.
|
|
156
|
+
|
|
157
|
+
## Data
|
|
87
158
|
|
|
88
|
-
|
|
159
|
+
Everything lives in `~/.scope/`:
|
|
89
160
|
|
|
90
|
-
|
|
161
|
+
```
|
|
162
|
+
~/.scope/
|
|
163
|
+
├── config.toml # configuration
|
|
164
|
+
├── contexts/ # saved project contexts
|
|
165
|
+
├── snapshots/ # daily snapshots (for review comparison)
|
|
166
|
+
├── muted.json # snoozed/muted items
|
|
167
|
+
├── notifications.log # notification history
|
|
168
|
+
└── daemon.pid # background process
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## License
|
|
91
172
|
|
|
92
|
-
|
|
173
|
+
MIT — [Fureworks](https://fureworks.com)
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
type JsonOptions = {
|
|
3
|
+
json?: boolean;
|
|
4
|
+
dir?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function configCommand(key?: string, value?: string, options?: JsonOptions): Promise<void>;
|
|
7
|
+
export declare function registerConfigCommand(program: Command): void;
|
|
8
|
+
export {};
|
|
2
9
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/cli/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,KAAK,WAAW,GAAG;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AA4HF,wBAAsB,aAAa,CACjC,GAAG,CAAC,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,IAAI,CAAC,CA+Cf;AAkND,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwE5D"}
|
package/dist/cli/config.js
CHANGED
|
@@ -1,54 +1,380 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, statSync, } from "node:fs";
|
|
5
3
|
import { homedir } from "node:os";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { configExists, loadConfig, saveConfig } from "../store/config.js";
|
|
6
|
+
function output(json, payload, text) {
|
|
7
|
+
if (json) {
|
|
8
|
+
console.log(JSON.stringify(payload));
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
console.log(text);
|
|
12
|
+
}
|
|
13
|
+
function fail(json, message, payload) {
|
|
14
|
+
if (json) {
|
|
15
|
+
console.log(JSON.stringify({
|
|
16
|
+
ok: false,
|
|
17
|
+
error: message,
|
|
18
|
+
...(payload && typeof payload === "object" ? payload : {}),
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.error(message);
|
|
23
|
+
}
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
function expandPath(input) {
|
|
27
|
+
if (input === "~") {
|
|
28
|
+
return homedir();
|
|
29
|
+
}
|
|
30
|
+
if (input.startsWith("~/")) {
|
|
31
|
+
return join(homedir(), input.slice(2));
|
|
32
|
+
}
|
|
33
|
+
return input;
|
|
34
|
+
}
|
|
35
|
+
function toAbsolutePath(input) {
|
|
36
|
+
const expanded = expandPath(input);
|
|
37
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
38
|
+
}
|
|
39
|
+
function parseScalar(value) {
|
|
40
|
+
if (value === "true")
|
|
41
|
+
return true;
|
|
42
|
+
if (value === "false")
|
|
43
|
+
return false;
|
|
44
|
+
const parsedNumber = Number(value);
|
|
45
|
+
if (!Number.isNaN(parsedNumber) && value.trim() !== "") {
|
|
46
|
+
return parsedNumber;
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function setConfigValue(config, key, value) {
|
|
51
|
+
const parts = key.split(".").filter(Boolean);
|
|
52
|
+
if (parts.length === 0) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const parsedValue = parseScalar(value);
|
|
56
|
+
let cursor = config;
|
|
57
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
58
|
+
const part = parts[i];
|
|
59
|
+
const existing = cursor[part];
|
|
60
|
+
if (!existing || typeof existing !== "object") {
|
|
61
|
+
cursor[part] = {};
|
|
62
|
+
}
|
|
63
|
+
cursor = cursor[part];
|
|
64
|
+
}
|
|
65
|
+
cursor[parts[parts.length - 1]] = parsedValue;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
function runCommand(command) {
|
|
69
|
+
try {
|
|
70
|
+
const stdout = execSync(command, { stdio: "pipe", encoding: "utf8" });
|
|
71
|
+
return { ok: true, output: stdout.trim() };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
return { ok: false, output: message.trim() };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function collectGitRepos(rootDir) {
|
|
79
|
+
const repos = new Set();
|
|
80
|
+
const queue = [rootDir];
|
|
81
|
+
while (queue.length > 0) {
|
|
82
|
+
const current = queue.pop();
|
|
83
|
+
if (!current)
|
|
84
|
+
continue;
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = readdirSync(current);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const fullPath = join(current, entry);
|
|
94
|
+
let stats;
|
|
95
|
+
try {
|
|
96
|
+
stats = lstatSync(fullPath);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (stats.isSymbolicLink()) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (entry === ".git" && stats.isDirectory()) {
|
|
105
|
+
repos.add(current);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (stats.isDirectory() && entry !== ".git") {
|
|
109
|
+
queue.push(fullPath);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return [...repos];
|
|
114
|
+
}
|
|
115
|
+
export async function configCommand(key, value, options = {}) {
|
|
116
|
+
const json = Boolean(options.json);
|
|
12
117
|
if (!key) {
|
|
118
|
+
if (json) {
|
|
119
|
+
output(true, { ok: true, config: loadConfig() }, "");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!configExists()) {
|
|
123
|
+
console.log("No config found");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
13
126
|
const configPath = join(homedir(), ".scope", "config.toml");
|
|
14
127
|
try {
|
|
15
|
-
const content = readFileSync(configPath, "utf-8");
|
|
16
|
-
console.log(
|
|
17
|
-
console.log(chalk.bold(" ~/.scope/config.toml"));
|
|
18
|
-
console.log(chalk.dim(" ─────────────────────\n"));
|
|
19
|
-
console.log(content
|
|
20
|
-
.split("\n")
|
|
21
|
-
.map((line) => ` ${line}`)
|
|
22
|
-
.join("\n"));
|
|
23
|
-
console.log("");
|
|
128
|
+
const content = readFileSync(configPath, "utf-8").trimEnd();
|
|
129
|
+
console.log(content);
|
|
24
130
|
}
|
|
25
131
|
catch {
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log(chalk.dim(" npm install -g @googleworkspace/cli"));
|
|
39
|
-
console.log(chalk.dim(" Then re-run: scope onboard\n"));
|
|
40
|
-
break;
|
|
41
|
-
case "projects":
|
|
42
|
-
const config = loadConfig();
|
|
43
|
-
console.log(chalk.bold("\n Projects:"));
|
|
44
|
-
for (const [name, project] of Object.entries(config.projects)) {
|
|
45
|
-
console.log(` ${name} → ${project.path}`);
|
|
132
|
+
fail(false, "Could not read config file");
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (value === undefined) {
|
|
137
|
+
const config = loadConfig();
|
|
138
|
+
const parts = key.split(".").filter(Boolean);
|
|
139
|
+
let cursor = config;
|
|
140
|
+
for (const part of parts) {
|
|
141
|
+
if (!cursor || typeof cursor !== "object" || !(part in cursor)) {
|
|
142
|
+
fail(json, `Unknown config key: ${key}`);
|
|
143
|
+
return;
|
|
46
144
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
145
|
+
cursor = cursor[part];
|
|
146
|
+
}
|
|
147
|
+
output(json, { ok: true, key, value: cursor }, `${key}=${String(cursor)}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const config = loadConfig();
|
|
151
|
+
const updated = setConfigValue(config, key, value);
|
|
152
|
+
if (!updated) {
|
|
153
|
+
fail(json, `Invalid config key: ${key}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
saveConfig(config);
|
|
157
|
+
output(json, { ok: true, key, value: parseScalar(value) }, "Config updated");
|
|
158
|
+
}
|
|
159
|
+
async function reposAdd(paths, options) {
|
|
160
|
+
const json = Boolean(options.json);
|
|
161
|
+
const config = loadConfig();
|
|
162
|
+
const existing = new Set(config.repos);
|
|
163
|
+
const added = [];
|
|
164
|
+
const invalid = [];
|
|
165
|
+
for (const rawPath of paths) {
|
|
166
|
+
const absolutePath = toAbsolutePath(rawPath);
|
|
167
|
+
if (!existsSync(absolutePath)) {
|
|
168
|
+
invalid.push(absolutePath);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
let isDirectory = false;
|
|
172
|
+
try {
|
|
173
|
+
isDirectory = statSync(absolutePath).isDirectory();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
invalid.push(absolutePath);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (!isDirectory) {
|
|
180
|
+
invalid.push(absolutePath);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!existing.has(absolutePath)) {
|
|
184
|
+
existing.add(absolutePath);
|
|
185
|
+
added.push(absolutePath);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (invalid.length > 0) {
|
|
189
|
+
fail(json, `Invalid repo path(s): ${invalid.join(", ")}`, { invalid });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (added.length > 0) {
|
|
193
|
+
config.repos = [...existing];
|
|
194
|
+
saveConfig(config);
|
|
195
|
+
}
|
|
196
|
+
output(json, { ok: true, added, total: existing.size }, added.length > 0 ? `Added ${added.length} repo(s)` : "No changes");
|
|
197
|
+
}
|
|
198
|
+
async function reposRemove(path, options) {
|
|
199
|
+
const json = Boolean(options.json);
|
|
200
|
+
const absolutePath = toAbsolutePath(path);
|
|
201
|
+
const config = loadConfig();
|
|
202
|
+
const before = config.repos.length;
|
|
203
|
+
config.repos = config.repos.filter((repoPath) => repoPath !== absolutePath);
|
|
204
|
+
if (config.repos.length !== before) {
|
|
205
|
+
saveConfig(config);
|
|
206
|
+
}
|
|
207
|
+
output(json, {
|
|
208
|
+
ok: true,
|
|
209
|
+
removed: before !== config.repos.length ? absolutePath : null,
|
|
210
|
+
total: config.repos.length,
|
|
211
|
+
}, before !== config.repos.length ? "Repo removed" : "No changes");
|
|
212
|
+
}
|
|
213
|
+
async function reposList(options) {
|
|
214
|
+
const json = Boolean(options.json);
|
|
215
|
+
const config = loadConfig();
|
|
216
|
+
output(json, { ok: true, repos: config.repos }, config.repos.length > 0 ? config.repos.join(", ") : "No repos configured");
|
|
217
|
+
}
|
|
218
|
+
async function reposScan(directory, options) {
|
|
219
|
+
const json = Boolean(options.json);
|
|
220
|
+
const absoluteDirectory = toAbsolutePath(directory);
|
|
221
|
+
if (!existsSync(absoluteDirectory) || !statSync(absoluteDirectory).isDirectory()) {
|
|
222
|
+
fail(json, `Directory not found: ${absoluteDirectory}`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const found = collectGitRepos(absoluteDirectory);
|
|
226
|
+
const config = loadConfig();
|
|
227
|
+
const existing = new Set(config.repos);
|
|
228
|
+
const added = [];
|
|
229
|
+
for (const repoPath of found) {
|
|
230
|
+
if (!existing.has(repoPath)) {
|
|
231
|
+
existing.add(repoPath);
|
|
232
|
+
added.push(repoPath);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (added.length > 0) {
|
|
236
|
+
config.repos = [...existing];
|
|
237
|
+
saveConfig(config);
|
|
52
238
|
}
|
|
239
|
+
output(json, { ok: true, scanned: absoluteDirectory, found, added, total: existing.size }, added.length > 0 ? `Added ${added.length} repo(s)` : "No changes");
|
|
240
|
+
}
|
|
241
|
+
async function calendarSet(enabled, options) {
|
|
242
|
+
const json = Boolean(options.json);
|
|
243
|
+
const config = loadConfig();
|
|
244
|
+
const changed = config.calendar.enabled !== enabled;
|
|
245
|
+
config.calendar.enabled = enabled;
|
|
246
|
+
if (changed) {
|
|
247
|
+
saveConfig(config);
|
|
248
|
+
}
|
|
249
|
+
output(json, { ok: true, enabled }, changed ? `Calendar ${enabled ? "enabled" : "disabled"}` : "No changes");
|
|
250
|
+
}
|
|
251
|
+
async function calendarTest(options) {
|
|
252
|
+
const json = Boolean(options.json);
|
|
253
|
+
const result = runCommand("gws --help");
|
|
254
|
+
if (!result.ok) {
|
|
255
|
+
fail(json, "Calendar test failed");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
output(json, { ok: true, backend: "gws" }, "Calendar test passed");
|
|
259
|
+
}
|
|
260
|
+
async function githubTest(options) {
|
|
261
|
+
const json = Boolean(options.json);
|
|
262
|
+
const result = runCommand("gh auth status");
|
|
263
|
+
if (!result.ok) {
|
|
264
|
+
fail(json, "GitHub test failed");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
output(json, { ok: true }, "GitHub auth is valid");
|
|
268
|
+
}
|
|
269
|
+
async function projectsAdd(name, options) {
|
|
270
|
+
const json = Boolean(options.json);
|
|
271
|
+
if (!options.dir) {
|
|
272
|
+
fail(json, "--dir is required");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const absoluteDirectory = toAbsolutePath(options.dir);
|
|
276
|
+
if (!existsSync(absoluteDirectory) || !statSync(absoluteDirectory).isDirectory()) {
|
|
277
|
+
fail(json, `Directory not found: ${absoluteDirectory}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const config = loadConfig();
|
|
281
|
+
const existing = config.projects[name];
|
|
282
|
+
const changed = !existing || existing.path !== absoluteDirectory;
|
|
283
|
+
config.projects[name] = { path: absoluteDirectory };
|
|
284
|
+
if (changed) {
|
|
285
|
+
saveConfig(config);
|
|
286
|
+
}
|
|
287
|
+
output(json, { ok: true, name, path: absoluteDirectory }, changed ? "Project saved" : "No changes");
|
|
288
|
+
}
|
|
289
|
+
async function projectsRemove(name, options) {
|
|
290
|
+
const json = Boolean(options.json);
|
|
291
|
+
const config = loadConfig();
|
|
292
|
+
if (config.projects[name]) {
|
|
293
|
+
delete config.projects[name];
|
|
294
|
+
saveConfig(config);
|
|
295
|
+
output(json, { ok: true, removed: name }, "Project removed");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
output(json, { ok: true, removed: null }, "No changes");
|
|
299
|
+
}
|
|
300
|
+
async function projectsList(options) {
|
|
301
|
+
const json = Boolean(options.json);
|
|
302
|
+
const config = loadConfig();
|
|
303
|
+
const projects = Object.entries(config.projects).map(([name, project]) => ({
|
|
304
|
+
name,
|
|
305
|
+
path: project.path,
|
|
306
|
+
}));
|
|
307
|
+
output(json, { ok: true, projects }, projects.length > 0
|
|
308
|
+
? projects.map((project) => `${project.name}=${project.path}`).join(", ")
|
|
309
|
+
: "No projects configured");
|
|
310
|
+
}
|
|
311
|
+
export function registerConfigCommand(program) {
|
|
312
|
+
const config = program
|
|
313
|
+
.command("config")
|
|
314
|
+
.description("View or edit configuration")
|
|
315
|
+
.argument("[key]")
|
|
316
|
+
.argument("[value]")
|
|
317
|
+
.option("--json", "Output as JSON")
|
|
318
|
+
.action(configCommand);
|
|
319
|
+
const repos = config.command("repos").description("Manage watched repos");
|
|
320
|
+
repos
|
|
321
|
+
.command("add <path...>")
|
|
322
|
+
.description("Add one or more repo paths")
|
|
323
|
+
.option("--json", "Output as JSON")
|
|
324
|
+
.action(reposAdd);
|
|
325
|
+
repos
|
|
326
|
+
.command("remove <path>")
|
|
327
|
+
.description("Remove a repo path")
|
|
328
|
+
.option("--json", "Output as JSON")
|
|
329
|
+
.action(reposRemove);
|
|
330
|
+
repos
|
|
331
|
+
.command("list")
|
|
332
|
+
.description("List watched repos")
|
|
333
|
+
.option("--json", "Output as JSON")
|
|
334
|
+
.action(reposList);
|
|
335
|
+
repos
|
|
336
|
+
.command("scan <directory>")
|
|
337
|
+
.description("Recursively scan for .git directories")
|
|
338
|
+
.option("--json", "Output as JSON")
|
|
339
|
+
.action(reposScan);
|
|
340
|
+
const calendar = config.command("calendar").description("Manage calendar integration");
|
|
341
|
+
calendar
|
|
342
|
+
.command("enable")
|
|
343
|
+
.description("Enable calendar integration")
|
|
344
|
+
.option("--json", "Output as JSON")
|
|
345
|
+
.action(async (options) => calendarSet(true, options));
|
|
346
|
+
calendar
|
|
347
|
+
.command("disable")
|
|
348
|
+
.description("Disable calendar integration")
|
|
349
|
+
.option("--json", "Output as JSON")
|
|
350
|
+
.action(async (options) => calendarSet(false, options));
|
|
351
|
+
calendar
|
|
352
|
+
.command("test")
|
|
353
|
+
.description("Test gws availability")
|
|
354
|
+
.option("--json", "Output as JSON")
|
|
355
|
+
.action(calendarTest);
|
|
356
|
+
const github = config.command("github").description("Manage GitHub integration");
|
|
357
|
+
github
|
|
358
|
+
.command("test")
|
|
359
|
+
.description("Test gh auth status")
|
|
360
|
+
.option("--json", "Output as JSON")
|
|
361
|
+
.action(githubTest);
|
|
362
|
+
const projects = config.command("projects").description("Manage projects");
|
|
363
|
+
projects
|
|
364
|
+
.command("add <name>")
|
|
365
|
+
.description("Add or update a project")
|
|
366
|
+
.requiredOption("--dir <path>", "Project directory")
|
|
367
|
+
.option("--json", "Output as JSON")
|
|
368
|
+
.action(projectsAdd);
|
|
369
|
+
projects
|
|
370
|
+
.command("remove <name>")
|
|
371
|
+
.description("Remove a project")
|
|
372
|
+
.option("--json", "Output as JSON")
|
|
373
|
+
.action(projectsRemove);
|
|
374
|
+
projects
|
|
375
|
+
.command("list")
|
|
376
|
+
.description("List projects")
|
|
377
|
+
.option("--json", "Output as JSON")
|
|
378
|
+
.action(projectsList);
|
|
53
379
|
}
|
|
54
380
|
//# sourceMappingURL=config.js.map
|