@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.
Files changed (80) hide show
  1. package/README.md +129 -48
  2. package/dist/cli/config.d.ts +8 -1
  3. package/dist/cli/config.d.ts.map +1 -1
  4. package/dist/cli/config.js +368 -42
  5. package/dist/cli/config.js.map +1 -1
  6. package/dist/cli/init.d.ts +2 -0
  7. package/dist/cli/init.d.ts.map +1 -0
  8. package/dist/cli/init.js +15 -0
  9. package/dist/cli/init.js.map +1 -0
  10. package/dist/cli/notifications.d.ts +7 -0
  11. package/dist/cli/notifications.d.ts.map +1 -0
  12. package/dist/cli/notifications.js +77 -0
  13. package/dist/cli/notifications.js.map +1 -0
  14. package/dist/cli/onboard.d.ts.map +1 -1
  15. package/dist/cli/onboard.js +2 -1
  16. package/dist/cli/onboard.js.map +1 -1
  17. package/dist/cli/plan.d.ts +7 -0
  18. package/dist/cli/plan.d.ts.map +1 -0
  19. package/dist/cli/plan.js +111 -0
  20. package/dist/cli/plan.js.map +1 -0
  21. package/dist/cli/review.d.ts +6 -0
  22. package/dist/cli/review.d.ts.map +1 -0
  23. package/dist/cli/review.js +167 -0
  24. package/dist/cli/review.js.map +1 -0
  25. package/dist/cli/snooze.d.ts +12 -0
  26. package/dist/cli/snooze.d.ts.map +1 -0
  27. package/dist/cli/snooze.js +155 -0
  28. package/dist/cli/snooze.js.map +1 -0
  29. package/dist/cli/today.d.ts.map +1 -1
  30. package/dist/cli/today.js +69 -9
  31. package/dist/cli/today.js.map +1 -1
  32. package/dist/cli/tune.d.ts +8 -0
  33. package/dist/cli/tune.d.ts.map +1 -0
  34. package/dist/cli/tune.js +62 -0
  35. package/dist/cli/tune.js.map +1 -0
  36. package/dist/engine/prioritize.d.ts +10 -2
  37. package/dist/engine/prioritize.d.ts.map +1 -1
  38. package/dist/engine/prioritize.js +156 -25
  39. package/dist/engine/prioritize.js.map +1 -1
  40. package/dist/index.js +48 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/notifications/index.d.ts.map +1 -1
  43. package/dist/notifications/index.js +6 -10
  44. package/dist/notifications/index.js.map +1 -1
  45. package/dist/sources/activity.d.ts +32 -0
  46. package/dist/sources/activity.d.ts.map +1 -0
  47. package/dist/sources/activity.js +101 -0
  48. package/dist/sources/activity.js.map +1 -0
  49. package/dist/sources/calendar.d.ts +6 -0
  50. package/dist/sources/calendar.d.ts.map +1 -1
  51. package/dist/sources/calendar.js +114 -0
  52. package/dist/sources/calendar.js.map +1 -1
  53. package/dist/store/config.d.ts +8 -0
  54. package/dist/store/config.d.ts.map +1 -1
  55. package/dist/store/config.js +22 -0
  56. package/dist/store/config.js.map +1 -1
  57. package/dist/store/muted.d.ts +17 -0
  58. package/dist/store/muted.d.ts.map +1 -0
  59. package/dist/store/muted.js +55 -0
  60. package/dist/store/muted.js.map +1 -0
  61. package/dist/store/snapshot.d.ts +12 -0
  62. package/dist/store/snapshot.d.ts.map +1 -0
  63. package/dist/store/snapshot.js +41 -0
  64. package/dist/store/snapshot.js.map +1 -0
  65. package/package.json +8 -2
  66. package/src/cli/config.ts +0 -66
  67. package/src/cli/context.ts +0 -109
  68. package/src/cli/daemon.ts +0 -217
  69. package/src/cli/onboard.ts +0 -335
  70. package/src/cli/status.ts +0 -77
  71. package/src/cli/switch.ts +0 -93
  72. package/src/cli/today.ts +0 -114
  73. package/src/engine/prioritize.ts +0 -257
  74. package/src/index.ts +0 -58
  75. package/src/notifications/index.ts +0 -42
  76. package/src/sources/calendar.ts +0 -170
  77. package/src/sources/git.ts +0 -168
  78. package/src/sources/issues.ts +0 -62
  79. package/src/store/config.ts +0 -104
  80. package/tsconfig.json +0 -19
package/README.md CHANGED
@@ -1,14 +1,8 @@
1
1
  # Scope
2
2
 
3
- **Personal ops CLIfocus on what matters.**
3
+ **Scope tells you the 3 things that matter right now and gives you permission to ignore everything else.**
4
4
 
5
- Scope reads your existing workflow (git repos, calendar, PRs) and tells you what actually needs your attention. It doesn't add to your workflow it gives you clarity.
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 # Guided setup (1 minute)
23
- scope today # What matters right now
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
- ## Commands
23
+ Or set up non-interactively (great for AI agents):
27
24
 
28
- ```
29
- scope onboard Guided first-time setup
30
- scope today What needs your attention right now
31
- scope status Overview of all watched projects
32
- scope switch <project> Switch to a project context
33
- scope context Show current project context
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
- ## How It Works
33
+ ## What It Does
39
34
 
40
- Scope reads signals from your existing tools:
35
+ Scope reads signals from tools you already use:
41
36
 
42
- - **Git** — uncommitted changes, stale branches, open PRs
43
- - **Google Calendar** — today's meetings, free blocks (via [gws](https://github.com/googleworkspace/cli))
44
- - **GitHub** — PR reviews waiting on you, failing CI
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
- It scores each item by urgency, staleness, and blocking potential, then shows you what matters:
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
- 🔴 Meeting: Team standup in 45 min
54
- 🔴 PR #8 on api-service waiting on your review (3 days)
50
+ 🔴 PR #8 on api-service (low context: no CI status)
51
+ auth migrationreview 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 — 3 uncommitted files, last touched 2h ago
59
- 🟡 PR #12 on fureworks.com open 3 days, no review
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
- 💡 2h free block after standup (14:00–16:00)
62
- Good for: fureworks/scope (has pending work)
99
+ Adjust weights with `scope tune`:
63
100
 
64
- 4 other items can wait → scope status
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
- ## Design Principles
107
+ ## Time Awareness
108
+
109
+ `scope today` adjusts its tone based on when you run it:
68
110
 
69
- - **CLI-first, local-first** your data stays on your machine
70
- - **Reads, doesn't create** no new inputs required from you
71
- - **Opinionated output** tells you what matters, not just lists stuff
72
- - **Degrades gracefully** — missing integrations skip quietly, never crash
73
- - **Open source** — MIT licensed
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
- - Node.js 18+
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
- ## Status
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
- 🚧 **v0.1 in development** — early but usable.
145
+ ## Best Practices
85
146
 
86
- ## License
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
- MIT see [LICENSE](./LICENSE)
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
- *A [Fureworks](https://fureworks.com) project — works born from human touch.*
173
+ MIT [Fureworks](https://fureworks.com)
@@ -1,2 +1,9 @@
1
- export declare function configCommand(key?: string, value?: string): Promise<void>;
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
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/cli/config.ts"],"names":[],"mappings":"AAMA,wBAAsB,aAAa,CACjC,GAAG,CAAC,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAwDf"}
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"}
@@ -1,54 +1,380 @@
1
- import chalk from "chalk";
2
- import { loadConfig, configExists } from "../store/config.js";
3
- import { readFileSync } from "node:fs";
4
- import { join } from "node:path";
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
- export async function configCommand(key, value) {
7
- if (!configExists()) {
8
- console.log(chalk.yellow("\n No config found. Run `scope onboard` to get started.\n"));
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
- // If no args, show config file contents
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
- console.log(chalk.yellow("\n Could not read config file.\n"));
27
- }
28
- return;
29
- }
30
- // Subcommands
31
- switch (key) {
32
- case "git":
33
- console.log(chalk.dim("\n To manage repos, edit ~/.scope/config.toml"));
34
- console.log(chalk.dim(" or re-run: scope onboard\n"));
35
- break;
36
- case "calendar":
37
- console.log(chalk.dim("\n To set up calendar, install gws:"));
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
- console.log(chalk.dim("\n Edit: ~/.scope/config.toml\n"));
48
- break;
49
- default:
50
- console.log(chalk.yellow(`\n Unknown config key: ${key}`));
51
- console.log(chalk.dim(" Available: git, calendar, projects\n"));
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