gitwrit 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 +122 -0
- package/bin/gitwrit.js +76 -0
- package/package.json +27 -0
- package/src/commands/add.js +44 -0
- package/src/commands/config.js +250 -0
- package/src/commands/help.js +53 -0
- package/src/commands/init.js +172 -0
- package/src/commands/logs.js +33 -0
- package/src/commands/remove.js +40 -0
- package/src/commands/restart.js +10 -0
- package/src/commands/start.js +118 -0
- package/src/commands/status.js +94 -0
- package/src/commands/stop.js +57 -0
- package/src/daemon.js +157 -0
- package/src/expansions.js +18 -0
- package/src/git.js +77 -0
- package/src/logger.js +53 -0
- package/src/paths.js +27 -0
- package/src/scheduler.js +46 -0
- package/src/settings.js +115 -0
- package/src/state.js +46 -0
- package/src/ui.js +63 -0
- package/src/watcher.js +59 -0
- package/src/words.json +128 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Terry Biddle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# gitwrit
|
|
2
|
+
|
|
3
|
+
**Private, versioned writing for people who live in the terminal.**
|
|
4
|
+
|
|
5
|
+
Made for `git`-ters and `writ`-ters.
|
|
6
|
+
|
|
7
|
+
**gitwrit** watches your Markdown files and quietly commits and pushes them to your Git repository — no manual `git add`, no `git commit`, no `git push`. You write. gitwrit handles the rest.
|
|
8
|
+
|
|
9
|
+
It is built for engineers writing internal specs, researchers building private knowledge bases, AI practitioners documenting models and experiments, and anyone who wants the safety of version control without the overhead of Git discipline. Your files stay in your own repository, on your own terms—not in someone else's cloud.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
gitwrit runs as a *background daemon*—but not the scary kind you have to exorcise. When you save a file, it waits a few seconds and then commits it locally. Every few minutes, it pushes everything to your remote. If your laptop sleeps, it catches up automatically when you wake it.
|
|
16
|
+
|
|
17
|
+
The result feels like writing in Google Docs. Your work is always saved—except as *plain* Markdown without artifacts that sometimes pop up in text editors when you export to Markdown. The files also remain *yours*, *where you want them*, and fully *versioned*.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
**npm**
|
|
24
|
+
```sh
|
|
25
|
+
npm install -g gitwrit
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Yarn**
|
|
29
|
+
```sh
|
|
30
|
+
yarn global add gitwrit
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
cd ~/notes
|
|
39
|
+
gitwrit init # first run sets up global defaults + registers this directory
|
|
40
|
+
gitwrit start # start watching
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That's it. Write something, save it, and gitwrit commits it.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
| Command | What it does |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `gitwrit init` | Set up gitwrit in the current directory |
|
|
52
|
+
| `gitwrit start` | Start watching your registered directories |
|
|
53
|
+
| `gitwrit stop` | Stop the daemon gracefully |
|
|
54
|
+
| `gitwrit restart` | Stop and restart (useful after config changes) |
|
|
55
|
+
| `gitwrit status` | Show what gitwrit is watching and what it has done lately |
|
|
56
|
+
| `gitwrit logs` | Tail the activity log |
|
|
57
|
+
| `gitwrit config` | Edit global defaults or local directory overrides |
|
|
58
|
+
| `gitwrit add [path]` | Add a directory to your watch list |
|
|
59
|
+
| `gitwrit remove [path]` | Remove a directory from your watch list |
|
|
60
|
+
| `gitwrit help` | List all available commands |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
gitwrit stores your global config at `~/.gitwritrc.json`. Run `gitwrit config` to edit it interactively.
|
|
67
|
+
|
|
68
|
+
**Global defaults:**
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"fileTypes": [".md", ".mdx"],
|
|
73
|
+
"debounce": 3000,
|
|
74
|
+
"pushInterval": 300000,
|
|
75
|
+
"branchMode": "current",
|
|
76
|
+
"watch": []
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Per-directory overrides:**
|
|
81
|
+
|
|
82
|
+
Run `gitwrit config` from inside a watched directory to set local overrides. Overrides are stored in `.gitwrit.json` in that directory. Anything not overridden falls through to your global defaults.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Branch modes
|
|
87
|
+
|
|
88
|
+
gitwrit supports two branch modes, configurable globally or per directory.
|
|
89
|
+
|
|
90
|
+
**`current`** — Commits to whatever branch you currently have checked out. Switch branches freely; gitwrit follows you.
|
|
91
|
+
|
|
92
|
+
**`autogenerated`** — Creates a fresh named branch at the start of each session. Branch names are generated by combining a random adjective, noun, and verb — like `crimson-walrus-stumbling` or `teal-fox-bouncing`. 64,000 possible combinations. Good for keeping autosave history separate from your main branch.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Requirements
|
|
97
|
+
|
|
98
|
+
- Node.js ≥ 18
|
|
99
|
+
- Git
|
|
100
|
+
- A configured Git remote (`git remote add origin <url>`) — gitwrit requires one before it will initialize a directory
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Why not Google Docs, Notion, or Obsidian Sync?
|
|
105
|
+
|
|
106
|
+
Those tools store your files on their infrastructure. gitwrit does not. Your writing lives in a plain Git repository that you control, pushed wherever you want—GitHub, GitLab, a private server, anywhere that accepts a Git remote.
|
|
107
|
+
|
|
108
|
+
Just to be clear, there's defnitely nothing wrong with using any of these tools. I use them!
|
|
109
|
+
|
|
110
|
+
But I thought making this might be especially useful for proprietary documentation like internal specs, research notes, model cards, experiment logs, etc. For when putting files into a third-party cloud is not an option, or when you want to keep everything essential to your codebase **all in one place**.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Open source
|
|
115
|
+
|
|
116
|
+
gitwrit is open source and built to be extended. Contributions are welcome at every level—word lists, new file type support, commit message formatters, and more. See [CONTRIBUTING.md](CONTRIBUTING.md) and [EXTENSIONS.md](EXTENSIONS.md) to get started.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
package/bin/gitwrit.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { init } from '../src/commands/init.js';
|
|
5
|
+
import { start } from '../src/commands/start.js';
|
|
6
|
+
import { stop } from '../src/commands/stop.js';
|
|
7
|
+
import { restart } from '../src/commands/restart.js';
|
|
8
|
+
import { status } from '../src/commands/status.js';
|
|
9
|
+
import { config } from '../src/commands/config.js';
|
|
10
|
+
import { add } from '../src/commands/add.js';
|
|
11
|
+
import { remove } from '../src/commands/remove.js';
|
|
12
|
+
import { logs } from '../src/commands/logs.js';
|
|
13
|
+
import { help } from '../src/commands/help.js';
|
|
14
|
+
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name('gitwrit')
|
|
19
|
+
.description('private, versioned writing for people who live in the terminal.')
|
|
20
|
+
.version('0.1.0')
|
|
21
|
+
.addHelpCommand(false) // we handle help ourselves
|
|
22
|
+
.helpOption(false);
|
|
23
|
+
|
|
24
|
+
program.command('init')
|
|
25
|
+
.description('set up gitwrit in the current directory')
|
|
26
|
+
.action(init);
|
|
27
|
+
|
|
28
|
+
program.command('start')
|
|
29
|
+
.description('start watching your registered directories')
|
|
30
|
+
.action(start);
|
|
31
|
+
|
|
32
|
+
program.command('stop')
|
|
33
|
+
.description('stop the daemon gracefully')
|
|
34
|
+
.action(stop);
|
|
35
|
+
|
|
36
|
+
program.command('restart')
|
|
37
|
+
.description('stop and restart the daemon')
|
|
38
|
+
.action(restart);
|
|
39
|
+
|
|
40
|
+
program.command('status')
|
|
41
|
+
.description('show running state and recent activity')
|
|
42
|
+
.action(status);
|
|
43
|
+
|
|
44
|
+
program.command('config')
|
|
45
|
+
.description('edit global defaults or local directory overrides')
|
|
46
|
+
.action(config);
|
|
47
|
+
|
|
48
|
+
program.command('add [path]')
|
|
49
|
+
.description('add a directory to your watch list')
|
|
50
|
+
.action(add);
|
|
51
|
+
|
|
52
|
+
program.command('remove [path]')
|
|
53
|
+
.description('remove a directory from your watch list')
|
|
54
|
+
.action(remove);
|
|
55
|
+
|
|
56
|
+
program.command('logs')
|
|
57
|
+
.description('tail the activity log')
|
|
58
|
+
.option('-n, --lines <number>', 'number of lines to show', '20')
|
|
59
|
+
.action((opts) => logs(parseInt(opts.lines, 10)));
|
|
60
|
+
|
|
61
|
+
program.command('help')
|
|
62
|
+
.description('show all commands')
|
|
63
|
+
.action(help);
|
|
64
|
+
|
|
65
|
+
// ── hidden daemon command ─────────────────────────────────────────────────────
|
|
66
|
+
// not shown in help — only called internally by `start`
|
|
67
|
+
program.command('__daemon', { hidden: true })
|
|
68
|
+
.action(() => import('../src/daemon.js'));
|
|
69
|
+
|
|
70
|
+
// ── fallback: no command given ────────────────────────────────────────────────
|
|
71
|
+
if (process.argv.length <= 2) {
|
|
72
|
+
help();
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gitwrit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Private, versioned writing for people who live in the terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gitwrit": "bin/gitwrit.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node bin/gitwrit.js",
|
|
18
|
+
"test": "node --test test/integration.test.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@inquirer/prompts": "^5.0.0",
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"chokidar": "^3.6.0",
|
|
24
|
+
"commander": "^12.1.0",
|
|
25
|
+
"simple-git": "^3.27.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { cwd } from 'process';
|
|
3
|
+
import { print } from '../ui.js';
|
|
4
|
+
import { addWatchPath, globalConfigExists } from '../settings.js';
|
|
5
|
+
import { isGitRepo, hasRemote } from '../git.js';
|
|
6
|
+
|
|
7
|
+
export async function add(dirArg) {
|
|
8
|
+
const dir = resolve(dirArg || cwd());
|
|
9
|
+
|
|
10
|
+
if (!(await globalConfigExists())) {
|
|
11
|
+
print.gap();
|
|
12
|
+
print.bad('gitwrit is not set up yet.');
|
|
13
|
+
print.hint('Run gitwrit init first.');
|
|
14
|
+
print.gap();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
print.gap();
|
|
19
|
+
|
|
20
|
+
if (!(await isGitRepo(dir))) {
|
|
21
|
+
print.bad(`${dir} is not a Git repo.`);
|
|
22
|
+
print.hint('Run git init there first, then try again.');
|
|
23
|
+
print.gap();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!(await hasRemote(dir))) {
|
|
28
|
+
print.bad('No Git remote configured.');
|
|
29
|
+
print.hint('Add a remote first: git remote add origin <url>');
|
|
30
|
+
print.gap();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const added = await addWatchPath({ type: 'directory', path: dir });
|
|
35
|
+
|
|
36
|
+
if (!added) {
|
|
37
|
+
print.warn(`${dir} is already in your watch list.`);
|
|
38
|
+
} else {
|
|
39
|
+
print.good(`${dir} added to your watch list.`);
|
|
40
|
+
print.hint('Run gitwrit restart to start watching it.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
print.gap();
|
|
44
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { select, confirm, checkbox, input } from '@inquirer/prompts';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { cwd } from 'process';
|
|
4
|
+
|
|
5
|
+
import { print } from '../ui.js';
|
|
6
|
+
import {
|
|
7
|
+
loadGlobalConfig,
|
|
8
|
+
saveGlobalConfig,
|
|
9
|
+
loadLocalConfig,
|
|
10
|
+
saveLocalConfig,
|
|
11
|
+
globalConfigExists,
|
|
12
|
+
} from '../settings.js';
|
|
13
|
+
|
|
14
|
+
// ─── field definitions ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const FIELDS = [
|
|
17
|
+
{ key: 'fileTypes', label: 'File types' },
|
|
18
|
+
{ key: 'debounce', label: 'Commit delay' },
|
|
19
|
+
{ key: 'pushInterval', label: 'Push interval' },
|
|
20
|
+
{ key: 'branchMode', label: 'Branch mode' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// ─── field editors ────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
async function editField(key, currentValue) {
|
|
26
|
+
if (key === 'fileTypes') {
|
|
27
|
+
return checkbox({
|
|
28
|
+
message: 'Which file types should gitwrit watch?',
|
|
29
|
+
choices: [
|
|
30
|
+
{ name: '.md', value: '.md', checked: currentValue.includes('.md') },
|
|
31
|
+
{ name: '.mdx', value: '.mdx', checked: currentValue.includes('.mdx') },
|
|
32
|
+
{ name: '.txt', value: '.txt', checked: currentValue.includes('.txt') },
|
|
33
|
+
{ name: '.rst', value: '.rst', checked: currentValue.includes('.rst') },
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (key === 'debounce') {
|
|
39
|
+
const choice = await select({
|
|
40
|
+
message: 'How long should gitwrit wait after a save before committing?',
|
|
41
|
+
choices: [
|
|
42
|
+
{ name: '3 seconds (recommended)', value: 3000 },
|
|
43
|
+
{ name: '5 seconds', value: 5000 },
|
|
44
|
+
{ name: '10 seconds', value: 10000 },
|
|
45
|
+
{ name: 'Custom', value: 'custom' },
|
|
46
|
+
],
|
|
47
|
+
default: currentValue,
|
|
48
|
+
});
|
|
49
|
+
if (choice === 'custom') {
|
|
50
|
+
const raw = await input({ message: 'Seconds to wait:' });
|
|
51
|
+
return parseFloat(raw) * 1000;
|
|
52
|
+
}
|
|
53
|
+
return choice;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (key === 'pushInterval') {
|
|
57
|
+
const choice = await select({
|
|
58
|
+
message: 'How often should gitwrit push to remote?',
|
|
59
|
+
choices: [
|
|
60
|
+
{ name: 'Every 5 minutes (recommended)', value: 300000 },
|
|
61
|
+
{ name: 'Every 10 minutes', value: 600000 },
|
|
62
|
+
{ name: 'Every 30 minutes', value: 1800000 },
|
|
63
|
+
{ name: 'Custom', value: 'custom' },
|
|
64
|
+
],
|
|
65
|
+
default: currentValue,
|
|
66
|
+
});
|
|
67
|
+
if (choice === 'custom') {
|
|
68
|
+
const raw = await input({ message: 'Minutes between pushes:' });
|
|
69
|
+
return parseFloat(raw) * 60000;
|
|
70
|
+
}
|
|
71
|
+
return choice;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (key === 'branchMode') {
|
|
75
|
+
return select({
|
|
76
|
+
message: 'Default branch mode?',
|
|
77
|
+
choices: [
|
|
78
|
+
{ name: "Current branch — commit to whatever branch you're on", value: 'current' },
|
|
79
|
+
{ name: 'Autogenerated — create a fresh named branch each session', value: 'autogenerated' },
|
|
80
|
+
],
|
|
81
|
+
default: currentValue,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── format a setting value for display ───────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function formatValue(key, value) {
|
|
89
|
+
if (key === 'fileTypes') return value.join(', ');
|
|
90
|
+
if (key === 'debounce') return `${value / 1000}s after last save`;
|
|
91
|
+
if (key === 'pushInterval') return `Every ${value / 60000} min`;
|
|
92
|
+
if (key === 'branchMode') return value;
|
|
93
|
+
return String(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── global config flow ───────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
async function configureGlobal() {
|
|
99
|
+
print.divider('Global defaults');
|
|
100
|
+
print.gap();
|
|
101
|
+
|
|
102
|
+
const config = await loadGlobalConfig();
|
|
103
|
+
|
|
104
|
+
for (const { key, label } of FIELDS) {
|
|
105
|
+
print.row(label, formatValue(key, config[key]));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
print.gap();
|
|
109
|
+
|
|
110
|
+
const field = await select({
|
|
111
|
+
message: 'Which setting would you like to change?',
|
|
112
|
+
choices: [
|
|
113
|
+
...FIELDS.map(f => ({ name: f.label, value: f.key })),
|
|
114
|
+
{ name: 'Done', value: null },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!field) return;
|
|
119
|
+
|
|
120
|
+
const newValue = await editField(field, config[field]);
|
|
121
|
+
config[field] = newValue;
|
|
122
|
+
await saveGlobalConfig(config);
|
|
123
|
+
|
|
124
|
+
print.gap();
|
|
125
|
+
print.good('Global defaults updated.');
|
|
126
|
+
print.hint('Directories without local overrides will use these settings.');
|
|
127
|
+
|
|
128
|
+
print.gap();
|
|
129
|
+
const again = await confirm({ message: 'Change another setting?', default: false });
|
|
130
|
+
if (again) await configureGlobal();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── local config flow ────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
async function configureLocal(dir) {
|
|
136
|
+
print.divider(`${dir} — local config`);
|
|
137
|
+
print.gap();
|
|
138
|
+
|
|
139
|
+
const global = await loadGlobalConfig();
|
|
140
|
+
const local = await loadLocalConfig(dir) || {};
|
|
141
|
+
const hasOverrides = Object.keys(local).length > 0;
|
|
142
|
+
|
|
143
|
+
for (const { key, label } of FIELDS) {
|
|
144
|
+
const isOverridden = key in local;
|
|
145
|
+
const value = isOverridden ? local[key] : global[key];
|
|
146
|
+
const tag = isOverridden ? 'local override ✎' : 'global';
|
|
147
|
+
print.row(label, formatValue(key, value), tag);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
print.gap();
|
|
151
|
+
|
|
152
|
+
if (!hasOverrides) {
|
|
153
|
+
print.info('This directory is following all global defaults.');
|
|
154
|
+
print.gap();
|
|
155
|
+
|
|
156
|
+
const wantsOverride = await confirm({
|
|
157
|
+
message: 'Set a local override?',
|
|
158
|
+
default: false,
|
|
159
|
+
});
|
|
160
|
+
if (!wantsOverride) return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const action = hasOverrides
|
|
164
|
+
? await select({
|
|
165
|
+
message: 'What would you like to do?',
|
|
166
|
+
choices: [
|
|
167
|
+
{ name: 'Edit an override', value: 'edit' },
|
|
168
|
+
{ name: 'Remove an override', value: 'remove' },
|
|
169
|
+
{ name: 'Add another override', value: 'add' },
|
|
170
|
+
],
|
|
171
|
+
})
|
|
172
|
+
: 'add';
|
|
173
|
+
|
|
174
|
+
if (action === 'edit' || action === 'add') {
|
|
175
|
+
const overriddenKeys = Object.keys(local);
|
|
176
|
+
const choices = action === 'edit'
|
|
177
|
+
? FIELDS.filter(f => overriddenKeys.includes(f.key))
|
|
178
|
+
: FIELDS.filter(f => !overriddenKeys.includes(f.key));
|
|
179
|
+
|
|
180
|
+
if (choices.length === 0) {
|
|
181
|
+
print.hint('Nothing to edit.');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const field = await select({
|
|
186
|
+
message: 'Which setting?',
|
|
187
|
+
choices: choices.map(f => ({ name: f.label, value: f.key })),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const currentValue = local[field] ?? global[field];
|
|
191
|
+
const newValue = await editField(field, currentValue);
|
|
192
|
+
local[field] = newValue;
|
|
193
|
+
await saveLocalConfig(dir, local);
|
|
194
|
+
print.gap();
|
|
195
|
+
print.good('Local override saved.');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (action === 'remove') {
|
|
199
|
+
const overriddenKeys = Object.keys(local);
|
|
200
|
+
const field = await select({
|
|
201
|
+
message: 'Which override would you like to remove?',
|
|
202
|
+
choices: FIELDS
|
|
203
|
+
.filter(f => overriddenKeys.includes(f.key))
|
|
204
|
+
.map(f => ({ name: f.label, value: f.key })),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
delete local[field];
|
|
208
|
+
await saveLocalConfig(dir, local);
|
|
209
|
+
print.gap();
|
|
210
|
+
print.good(`Local override for "${field}" removed — reverting to global default.`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
print.gap();
|
|
214
|
+
const again = await confirm({ message: 'Make another change?', default: false });
|
|
215
|
+
if (again) await configureLocal(dir);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── main ─────────────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
export async function config() {
|
|
221
|
+
if (!(await globalConfigExists())) {
|
|
222
|
+
print.gap();
|
|
223
|
+
print.bad('gitwrit is not set up yet.');
|
|
224
|
+
print.hint('Run gitwrit init first.');
|
|
225
|
+
print.gap();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const dir = resolve(cwd());
|
|
230
|
+
|
|
231
|
+
print.gap();
|
|
232
|
+
|
|
233
|
+
const scope = await select({
|
|
234
|
+
message: 'What would you like to configure?',
|
|
235
|
+
choices: [
|
|
236
|
+
{ name: 'Global defaults', value: 'global' },
|
|
237
|
+
{ name: `This directory (${dir})`, value: 'local' },
|
|
238
|
+
],
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
print.gap();
|
|
242
|
+
|
|
243
|
+
if (scope === 'global') {
|
|
244
|
+
await configureGlobal();
|
|
245
|
+
} else {
|
|
246
|
+
await configureLocal(dir);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
print.gap();
|
|
250
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { print } from '../ui.js';
|
|
3
|
+
|
|
4
|
+
const commands = [
|
|
5
|
+
{ name: 'init', desc: 'Set up gitwrit in the current directory' },
|
|
6
|
+
{ name: '', desc: chalk.dim('(First run also configures your global defaults)') },
|
|
7
|
+
{ name: 'start', desc: 'Start watching your registered directories' },
|
|
8
|
+
{ name: 'stop', desc: 'Stop the daemon gracefully' },
|
|
9
|
+
{ name: 'restart', desc: 'Stop and restart (useful after config changes)' },
|
|
10
|
+
{ name: '', desc: '' },
|
|
11
|
+
{ name: 'status', desc: 'Show what gitwrit is watching and what it\'s done lately' },
|
|
12
|
+
{ name: 'logs', desc: 'Tail the activity log' },
|
|
13
|
+
{ name: '', desc: '' },
|
|
14
|
+
{ name: 'config', desc: 'Edit your global defaults or local directory overrides' },
|
|
15
|
+
{ name: 'add', desc: 'Add a directory to your watch list' },
|
|
16
|
+
{ name: 'remove', desc: 'Remove a directory from your watch list' },
|
|
17
|
+
{ name: '', desc: '' },
|
|
18
|
+
{ name: 'help', desc: 'List all available commands' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const tips = [
|
|
22
|
+
'Run gitwrit init in any directory to register it.',
|
|
23
|
+
'Run gitwrit config from inside a watched directory to set local overrides.',
|
|
24
|
+
'gitwrit commits to whatever branch you\'re on — switch branches freely.',
|
|
25
|
+
'Branch mode "autogenerated" names your session branch automatically.',
|
|
26
|
+
'gitwrit requires a configured Git remote to push your work offsite.',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function help() {
|
|
30
|
+
print.gap();
|
|
31
|
+
print.brand('gitwrit — Private, versioned writing for people who live in the terminal.');
|
|
32
|
+
print.gap();
|
|
33
|
+
|
|
34
|
+
console.log(chalk.dim(' commands'));
|
|
35
|
+
print.gap();
|
|
36
|
+
|
|
37
|
+
for (const { name, desc } of commands) {
|
|
38
|
+
const k = chalk.white(name.padEnd(12));
|
|
39
|
+
console.log(` ${k}${desc}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
print.gap();
|
|
43
|
+
console.log(chalk.dim(' tips'));
|
|
44
|
+
print.gap();
|
|
45
|
+
|
|
46
|
+
for (const tip of tips) {
|
|
47
|
+
print.hint(`· ${tip}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
print.gap();
|
|
51
|
+
console.log(chalk.dim(' docs + source github.com/TBiddy/gitwrit'));
|
|
52
|
+
print.gap();
|
|
53
|
+
}
|