gitwrit 0.1.0 → 0.2.1
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 +16 -4
- package/bin/gitwrit.js +34 -15
- package/package.json +5 -1
- package/src/banner.js +51 -0
- package/src/commands/help.js +10 -8
- package/src/commands/init.js +18 -13
- package/src/commands/start.js +9 -10
- package/src/notify.js +34 -0
- package/src/scheduler.js +10 -11
- package/src/ui.js +18 -13
- package/src/updater.js +88 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Made for `git`-ters and `writ`-ters.
|
|
6
6
|
|
|
7
|
-
**gitwrit**
|
|
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
8
|
|
|
9
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
10
|
|
|
@@ -40,7 +40,7 @@ gitwrit init # first run sets up global defaults + registers this directory
|
|
|
40
40
|
gitwrit start # start watching
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
That's it. Write something, save it, and gitwrit commits it.
|
|
43
|
+
That's it. Write something, save it, and gitwrit commits it.
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
@@ -93,6 +93,18 @@ gitwrit supports two branch modes, configurable globally or per directory.
|
|
|
93
93
|
|
|
94
94
|
---
|
|
95
95
|
|
|
96
|
+
## Updates
|
|
97
|
+
|
|
98
|
+
gitwrit checks for updates automatically after every command. If a newer version is available, you will see a single note at the bottom of your output:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
· Update available: 0.2.1 → 0.3.0 Run npm install -g gitwrit to update.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The check runs in the background, caches for 24 hours, and fails silently if you are offline. It will never interrupt or slow down your workflow.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
96
108
|
## Requirements
|
|
97
109
|
|
|
98
110
|
- Node.js ≥ 18
|
|
@@ -105,7 +117,7 @@ gitwrit supports two branch modes, configurable globally or per directory.
|
|
|
105
117
|
|
|
106
118
|
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
119
|
|
|
108
|
-
Just to be clear, there
|
|
120
|
+
Just to be clear, there is absolutely nothing wrong with using any of these tools. I use them!
|
|
109
121
|
|
|
110
122
|
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
123
|
|
|
@@ -119,4 +131,4 @@ gitwrit is open source and built to be extended. Contributions are welcome at ev
|
|
|
119
131
|
|
|
120
132
|
## License
|
|
121
133
|
|
|
122
|
-
MIT
|
|
134
|
+
MIT
|
package/bin/gitwrit.js
CHANGED
|
@@ -11,59 +11,61 @@ import { add } from '../src/commands/add.js';
|
|
|
11
11
|
import { remove } from '../src/commands/remove.js';
|
|
12
12
|
import { logs } from '../src/commands/logs.js';
|
|
13
13
|
import { help } from '../src/commands/help.js';
|
|
14
|
+
import { checkForUpdate, currentVersion } from '../src/updater.js';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { TEAL, PINK } from '../src/ui.js';
|
|
14
17
|
|
|
15
18
|
const program = new Command();
|
|
16
19
|
|
|
17
20
|
program
|
|
18
21
|
.name('gitwrit')
|
|
19
|
-
.description('
|
|
20
|
-
.version('0.1
|
|
21
|
-
.addHelpCommand(false)
|
|
22
|
+
.description('Private, versioned writing for people who live in the terminal.')
|
|
23
|
+
.version('0.2.1')
|
|
24
|
+
.addHelpCommand(false)
|
|
22
25
|
.helpOption(false);
|
|
23
26
|
|
|
24
27
|
program.command('init')
|
|
25
|
-
.description('
|
|
28
|
+
.description('Set up gitwrit in the current directory')
|
|
26
29
|
.action(init);
|
|
27
30
|
|
|
28
31
|
program.command('start')
|
|
29
|
-
.description('
|
|
32
|
+
.description('Start watching your registered directories')
|
|
30
33
|
.action(start);
|
|
31
34
|
|
|
32
35
|
program.command('stop')
|
|
33
|
-
.description('
|
|
36
|
+
.description('Stop the daemon gracefully')
|
|
34
37
|
.action(stop);
|
|
35
38
|
|
|
36
39
|
program.command('restart')
|
|
37
|
-
.description('
|
|
40
|
+
.description('Stop and restart the daemon')
|
|
38
41
|
.action(restart);
|
|
39
42
|
|
|
40
43
|
program.command('status')
|
|
41
|
-
.description('
|
|
44
|
+
.description('Show running state and recent activity')
|
|
42
45
|
.action(status);
|
|
43
46
|
|
|
44
47
|
program.command('config')
|
|
45
|
-
.description('
|
|
48
|
+
.description('Edit global defaults or local directory overrides')
|
|
46
49
|
.action(config);
|
|
47
50
|
|
|
48
51
|
program.command('add [path]')
|
|
49
|
-
.description('
|
|
52
|
+
.description('Add a directory to your watch list')
|
|
50
53
|
.action(add);
|
|
51
54
|
|
|
52
55
|
program.command('remove [path]')
|
|
53
|
-
.description('
|
|
56
|
+
.description('Remove a directory from your watch list')
|
|
54
57
|
.action(remove);
|
|
55
58
|
|
|
56
59
|
program.command('logs')
|
|
57
|
-
.description('
|
|
60
|
+
.description('Tail the activity log')
|
|
58
61
|
.option('-n, --lines <number>', 'number of lines to show', '20')
|
|
59
62
|
.action((opts) => logs(parseInt(opts.lines, 10)));
|
|
60
63
|
|
|
61
64
|
program.command('help')
|
|
62
|
-
.description('
|
|
65
|
+
.description('List all available commands')
|
|
63
66
|
.action(help);
|
|
64
67
|
|
|
65
68
|
// ── hidden daemon command ─────────────────────────────────────────────────────
|
|
66
|
-
// not shown in help — only called internally by `start`
|
|
67
69
|
program.command('__daemon', { hidden: true })
|
|
68
70
|
.action(() => import('../src/daemon.js'));
|
|
69
71
|
|
|
@@ -73,4 +75,21 @@ if (process.argv.length <= 2) {
|
|
|
73
75
|
process.exit(0);
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
// ── run command then check for updates ───────────────────────────────────────
|
|
79
|
+
// update check runs after the command completes so it never delays the user.
|
|
80
|
+
// shows a single hint line at the bottom if a newer version is available.
|
|
81
|
+
program.parseAsync(process.argv).then(async () => {
|
|
82
|
+
const latest = await checkForUpdate();
|
|
83
|
+
if (latest) {
|
|
84
|
+
console.log(
|
|
85
|
+
'\n' +
|
|
86
|
+
chalk.dim(' · Update available: ') +
|
|
87
|
+
chalk.hex(TEAL)(currentVersion()) +
|
|
88
|
+
chalk.dim(' → ') +
|
|
89
|
+
chalk.hex(PINK).bold(latest) +
|
|
90
|
+
chalk.dim(' Run ') +
|
|
91
|
+
chalk.white('npm install -g gitwrit') +
|
|
92
|
+
chalk.dim(' to update.')
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitwrit",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Private, versioned writing for people who live in the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,9 +19,13 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@inquirer/prompts": "^5.0.0",
|
|
22
|
+
"boxen": "^7.1.1",
|
|
22
23
|
"chalk": "^5.3.0",
|
|
23
24
|
"chokidar": "^3.6.0",
|
|
24
25
|
"commander": "^12.1.0",
|
|
26
|
+
"figlet": "^1.11.0",
|
|
27
|
+
"gradient-string": "^2.0.2",
|
|
28
|
+
"ora": "^8.2.0",
|
|
25
29
|
"simple-git": "^3.27.0"
|
|
26
30
|
}
|
|
27
31
|
}
|
package/src/banner.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import figlet from 'figlet';
|
|
2
|
+
import gradient from 'gradient-string';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
// ─── palette ──────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const TEAL = '#2EC4B6';
|
|
8
|
+
const PINK = '#F72585';
|
|
9
|
+
|
|
10
|
+
const brandGradient = gradient(TEAL, PINK);
|
|
11
|
+
|
|
12
|
+
// ─── banner ───────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
// renders the full gitwrit banner:
|
|
15
|
+
// - BlurVision ASCII art in teal→pink gradient
|
|
16
|
+
// - tagline with git (teal) and writ (pink) colored separately
|
|
17
|
+
// - subtitle dimmed
|
|
18
|
+
//
|
|
19
|
+
// shown on:
|
|
20
|
+
// - gitwrit init (first run only)
|
|
21
|
+
// - gitwrit help
|
|
22
|
+
|
|
23
|
+
export function renderBanner() {
|
|
24
|
+
const ascii = figlet.textSync('gitwrit', {
|
|
25
|
+
font: 'BlurVision ASCII',
|
|
26
|
+
horizontalLayout: 'default',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// apply gradient line by line so the block characters catch
|
|
30
|
+
// the color transition at different densities (░ ▒ ▓)
|
|
31
|
+
const colored = brandGradient.multiline(ascii);
|
|
32
|
+
|
|
33
|
+
// tagline — git in teal, writ in pink, rest dimmed
|
|
34
|
+
const tagline =
|
|
35
|
+
chalk.dim(' Made for ') +
|
|
36
|
+
chalk.hex(TEAL).bold('`git`') +
|
|
37
|
+
chalk.dim('-ters and ') +
|
|
38
|
+
chalk.hex(PINK).bold('`writ`') +
|
|
39
|
+
chalk.dim('-ters.');
|
|
40
|
+
|
|
41
|
+
// subtitle
|
|
42
|
+
const subtitle = chalk.dim(
|
|
43
|
+
' Private, versioned writing for people who live in the terminal.'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(colored);
|
|
48
|
+
console.log(subtitle);
|
|
49
|
+
console.log(tagline);
|
|
50
|
+
console.log();
|
|
51
|
+
}
|
package/src/commands/help.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { print } from '../ui.js';
|
|
2
|
+
import { print, TEAL, PINK } from '../ui.js';
|
|
3
|
+
import { renderBanner } from '../banner.js';
|
|
3
4
|
|
|
4
5
|
const commands = [
|
|
5
6
|
{ name: 'init', desc: 'Set up gitwrit in the current directory' },
|
|
@@ -8,7 +9,7 @@ const commands = [
|
|
|
8
9
|
{ name: 'stop', desc: 'Stop the daemon gracefully' },
|
|
9
10
|
{ name: 'restart', desc: 'Stop and restart (useful after config changes)' },
|
|
10
11
|
{ name: '', desc: '' },
|
|
11
|
-
{ name: 'status', desc: 'Show what gitwrit is watching and what it\'s done lately'
|
|
12
|
+
{ name: 'status', desc: 'Show what gitwrit is watching and what it\'s done lately' },
|
|
12
13
|
{ name: 'logs', desc: 'Tail the activity log' },
|
|
13
14
|
{ name: '', desc: '' },
|
|
14
15
|
{ name: 'config', desc: 'Edit your global defaults or local directory overrides' },
|
|
@@ -27,15 +28,13 @@ const tips = [
|
|
|
27
28
|
];
|
|
28
29
|
|
|
29
30
|
export function help() {
|
|
30
|
-
|
|
31
|
-
print.brand('gitwrit — Private, versioned writing for people who live in the terminal.');
|
|
32
|
-
print.gap();
|
|
31
|
+
renderBanner();
|
|
33
32
|
|
|
34
33
|
console.log(chalk.dim(' commands'));
|
|
35
34
|
print.gap();
|
|
36
35
|
|
|
37
36
|
for (const { name, desc } of commands) {
|
|
38
|
-
const k = chalk.
|
|
37
|
+
const k = chalk.hex(TEAL)(name.padEnd(12));
|
|
39
38
|
console.log(` ${k}${desc}`);
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -48,6 +47,9 @@ export function help() {
|
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
print.gap();
|
|
51
|
-
console.log(
|
|
50
|
+
console.log(
|
|
51
|
+
chalk.dim(' docs + source ') +
|
|
52
|
+
chalk.hex(TEAL)('github.com/TBiddy/gitwrit')
|
|
53
|
+
);
|
|
52
54
|
print.gap();
|
|
53
|
-
}
|
|
55
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -2,8 +2,10 @@ import { checkbox, select, confirm, input } from '@inquirer/prompts';
|
|
|
2
2
|
import { mkdir } from 'fs/promises';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
4
|
import { cwd } from 'process';
|
|
5
|
+
import ora from 'ora';
|
|
5
6
|
|
|
6
|
-
import { print } from '../ui.js';
|
|
7
|
+
import { print, TEAL, PINK } from '../ui.js';
|
|
8
|
+
import { renderBanner } from '../banner.js';
|
|
7
9
|
import {
|
|
8
10
|
DEFAULTS,
|
|
9
11
|
globalConfigExists,
|
|
@@ -23,7 +25,8 @@ export async function init() {
|
|
|
23
25
|
// ── first-run: global setup ────────────────────────────────────────────────
|
|
24
26
|
|
|
25
27
|
if (isFirstRun) {
|
|
26
|
-
|
|
28
|
+
renderBanner();
|
|
29
|
+
print.brand('Welcome to gitwrit. Let\'s get you set up.');
|
|
27
30
|
print.gap();
|
|
28
31
|
print.divider('Global defaults');
|
|
29
32
|
print.hint('These apply to every directory you watch.');
|
|
@@ -77,7 +80,7 @@ export async function init() {
|
|
|
77
80
|
message: 'Default branch mode?',
|
|
78
81
|
choices: [
|
|
79
82
|
{
|
|
80
|
-
name:
|
|
83
|
+
name: 'Current branch — commit to whatever branch you\'re on',
|
|
81
84
|
value: 'current',
|
|
82
85
|
},
|
|
83
86
|
{
|
|
@@ -107,19 +110,23 @@ export async function init() {
|
|
|
107
110
|
print.divider(`Registering ${dir}`);
|
|
108
111
|
print.gap();
|
|
109
112
|
|
|
110
|
-
//
|
|
113
|
+
// validate repo with spinner
|
|
114
|
+
const spinner = ora({
|
|
115
|
+
text: 'Checking repository...',
|
|
116
|
+
color: 'cyan',
|
|
117
|
+
}).start();
|
|
118
|
+
|
|
111
119
|
const repoValid = await isGitRepo(dir);
|
|
112
120
|
if (!repoValid) {
|
|
113
|
-
|
|
121
|
+
spinner.fail('Not a Git repository.');
|
|
114
122
|
print.hint('Run git init here first, then run gitwrit init again.');
|
|
115
123
|
print.gap();
|
|
116
124
|
process.exit(1);
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
// hard gate: must have a remote
|
|
120
127
|
const remoteValid = await hasRemote(dir);
|
|
121
128
|
if (!remoteValid) {
|
|
122
|
-
|
|
129
|
+
spinner.fail('No Git remote configured.');
|
|
123
130
|
print.hint('gitwrit pushes your work to a remote repository to keep it safe.');
|
|
124
131
|
print.hint('Add one first: git remote add origin <url>');
|
|
125
132
|
print.hint('Then run gitwrit init again.');
|
|
@@ -127,11 +134,10 @@ export async function init() {
|
|
|
127
134
|
process.exit(1);
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
|
|
131
|
-
print.good('Remote origin configured.');
|
|
137
|
+
spinner.succeed('Valid Git repo with remote confirmed.');
|
|
132
138
|
print.gap();
|
|
133
139
|
|
|
134
|
-
// local overrides — only ask if not first run
|
|
140
|
+
// local overrides — only ask if not first run
|
|
135
141
|
let useGlobalDefaults = true;
|
|
136
142
|
if (!isFirstRun) {
|
|
137
143
|
useGlobalDefaults = await confirm({
|
|
@@ -144,7 +150,6 @@ export async function init() {
|
|
|
144
150
|
print.hint('Local overrides can be set anytime with gitwrit config.');
|
|
145
151
|
}
|
|
146
152
|
|
|
147
|
-
// register the directory in global watch list
|
|
148
153
|
const added = await addWatchPath({ type: 'directory', path: dir });
|
|
149
154
|
|
|
150
155
|
print.gap();
|
|
@@ -167,6 +172,6 @@ export async function init() {
|
|
|
167
172
|
print.row('Committing', `${config.debounce / 1000}s after last save`);
|
|
168
173
|
print.row('Pushing', `Every ${config.pushInterval / 60000} min`);
|
|
169
174
|
print.gap();
|
|
170
|
-
print.hint(
|
|
175
|
+
print.hint('Run gitwrit start whenever you\'re ready.');
|
|
171
176
|
print.gap();
|
|
172
|
-
}
|
|
177
|
+
}
|
package/src/commands/start.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import { writeFile, mkdir, readFile } from 'fs/promises';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
|
+
import ora from 'ora';
|
|
5
6
|
|
|
6
7
|
import { print } from '../ui.js';
|
|
7
8
|
import { PID_FILE, GITWRIT_DIR } from '../paths.js';
|
|
@@ -77,6 +78,12 @@ export async function start() {
|
|
|
77
78
|
|
|
78
79
|
await mkdir(GITWRIT_DIR, { recursive: true });
|
|
79
80
|
|
|
81
|
+
// spinner while daemon spins up
|
|
82
|
+
const spinner = ora({
|
|
83
|
+
text: 'Starting gitwrit...',
|
|
84
|
+
color: 'cyan',
|
|
85
|
+
}).start();
|
|
86
|
+
|
|
80
87
|
const daemon = spawn(process.execPath, [DAEMON_SCRIPT], {
|
|
81
88
|
detached: true,
|
|
82
89
|
stdio: 'ignore',
|
|
@@ -87,18 +94,10 @@ export async function start() {
|
|
|
87
94
|
});
|
|
88
95
|
|
|
89
96
|
daemon.unref();
|
|
90
|
-
|
|
91
97
|
await writeFile(PID_FILE, String(daemon.pid), 'utf8');
|
|
92
|
-
|
|
93
98
|
await new Promise(r => setTimeout(r, 300));
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (wasUnclean) {
|
|
98
|
-
print.brand('gitwrit is running — picked up where you left off.');
|
|
99
|
-
} else {
|
|
100
|
-
print.brand('gitwrit is running.');
|
|
101
|
-
}
|
|
100
|
+
spinner.succeed(wasUnclean ? 'Picked up where you left off.' : 'gitwrit is running.');
|
|
102
101
|
|
|
103
102
|
print.gap();
|
|
104
103
|
print.row('Watching', config.watch.map(w => w.path).join(', '));
|
|
@@ -115,4 +114,4 @@ export async function start() {
|
|
|
115
114
|
print.gap();
|
|
116
115
|
print.hint(randomStartMessage());
|
|
117
116
|
print.gap();
|
|
118
|
-
}
|
|
117
|
+
}
|
package/src/notify.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import { platform } from 'os';
|
|
3
|
+
|
|
4
|
+
// ─── native notification ───────────────────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// fires a native OS notification with no npm dependencies.
|
|
7
|
+
// macOS → osascript
|
|
8
|
+
// Linux → notify-send (degrades silently if not installed)
|
|
9
|
+
// other → silent, no crash
|
|
10
|
+
//
|
|
11
|
+
// only called in two places:
|
|
12
|
+
// 1. scheduler.js — on push failure (always notify)
|
|
13
|
+
// 2. nowhere else — stop summary lives in the terminal output only
|
|
14
|
+
|
|
15
|
+
export function notify(title, message) {
|
|
16
|
+
const os = platform();
|
|
17
|
+
|
|
18
|
+
// sanitize inputs — prevent shell injection via single-quote stripping
|
|
19
|
+
const safeTitle = title.replace(/'/g, '');
|
|
20
|
+
const safeMessage = message.replace(/'/g, '');
|
|
21
|
+
|
|
22
|
+
if (os === 'darwin') {
|
|
23
|
+
exec(
|
|
24
|
+
`osascript -e 'display notification "${safeMessage}" with title "${safeTitle}"'`,
|
|
25
|
+
() => {} // ignore errors — notification is best-effort
|
|
26
|
+
);
|
|
27
|
+
} else if (os === 'linux') {
|
|
28
|
+
exec(
|
|
29
|
+
`notify-send "${safeTitle}" "${safeMessage}" 2>/dev/null`,
|
|
30
|
+
() => {}
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
// windows + anything else — silent
|
|
34
|
+
}
|
package/src/scheduler.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { push, getUnpushedCount } from './git.js';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
3
|
import { writeState, readState } from './state.js';
|
|
4
|
+
import { notify } from './notify.js';
|
|
4
5
|
|
|
5
6
|
// ─── scheduler ────────────────────────────────────────────────────────────────
|
|
6
7
|
|
|
7
|
-
// maintains a set of repos that have unpushed commits and pushes them
|
|
8
|
-
// on a fixed interval. safe to call multiple times — push is idempotent.
|
|
9
8
|
export function createScheduler({ interval, onPushComplete }) {
|
|
10
9
|
const pendingRepos = new Set();
|
|
11
10
|
|
|
@@ -21,13 +20,17 @@ export function createScheduler({ interval, onPushComplete }) {
|
|
|
21
20
|
await push(dir);
|
|
22
21
|
pendingRepos.delete(dir);
|
|
23
22
|
await logger.push(`${dir}`);
|
|
24
|
-
|
|
25
|
-
const state = await readState();
|
|
26
23
|
await writeState({ lastPushedAt: new Date().toISOString() });
|
|
27
24
|
|
|
28
25
|
if (onPushComplete) onPushComplete(dir);
|
|
29
26
|
} catch (err) {
|
|
30
27
|
await logger.error(`push failed for ${dir}: ${err.message}`);
|
|
28
|
+
|
|
29
|
+
// native OS notification — only fires on failure
|
|
30
|
+
notify(
|
|
31
|
+
'gitwrit — Push failed',
|
|
32
|
+
'Your commits are safe locally. Check your network or remote config.'
|
|
33
|
+
);
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
}
|
|
@@ -35,12 +38,8 @@ export function createScheduler({ interval, onPushComplete }) {
|
|
|
35
38
|
const timer = setInterval(flush, interval);
|
|
36
39
|
|
|
37
40
|
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// flush immediately (e.g. on wake from sleep)
|
|
42
|
-
flushNow: () => flush(),
|
|
43
|
-
|
|
44
|
-
stop: () => clearInterval(timer),
|
|
41
|
+
queue: (dir) => pendingRepos.add(dir),
|
|
42
|
+
flushNow: () => flush(),
|
|
43
|
+
stop: () => clearInterval(timer),
|
|
45
44
|
};
|
|
46
45
|
}
|
package/src/ui.js
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
|
|
3
|
+
// ─── palette ──────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export const TEAL = '#2EC4B6';
|
|
6
|
+
export const PINK = '#F72585';
|
|
7
|
+
|
|
3
8
|
// ─── sigil + brand ────────────────────────────────────────────────────────────
|
|
4
9
|
|
|
5
10
|
export const SIGIL = '✦';
|
|
6
11
|
|
|
7
|
-
export const brand
|
|
8
|
-
export const info
|
|
9
|
-
export const good
|
|
10
|
-
export const bad
|
|
11
|
-
export const warn
|
|
12
|
-
export const hint
|
|
12
|
+
export const brand = (msg) => chalk.hex(TEAL)(`\n ${SIGIL} ${msg}`);
|
|
13
|
+
export const info = (msg) => chalk.white(` ${msg}`);
|
|
14
|
+
export const good = (msg) => chalk.hex(TEAL)(` ✔ ${msg}`);
|
|
15
|
+
export const bad = (msg) => chalk.red(` ✗ ${msg}`);
|
|
16
|
+
export const warn = (msg) => chalk.hex(PINK)(` ⚠ ${msg}`);
|
|
17
|
+
export const hint = (msg) => chalk.dim(` ${msg}`);
|
|
13
18
|
export const divider = (label) =>
|
|
14
|
-
chalk.dim(`\n ${'─'.repeat(41)}\n
|
|
19
|
+
chalk.hex(TEAL).dim(`\n ${'─'.repeat(41)}\n `) +
|
|
20
|
+
chalk.hex(TEAL)(label) +
|
|
21
|
+
chalk.hex(TEAL).dim(`\n ${'─'.repeat(41)}`);
|
|
15
22
|
|
|
16
23
|
// ─── key/value rows ───────────────────────────────────────────────────────────
|
|
17
24
|
|
|
18
|
-
// prints a two-column row like:
|
|
19
|
-
// watching ~/notes
|
|
20
25
|
export function row(key, value, tag) {
|
|
21
26
|
const k = chalk.dim(key.padEnd(14));
|
|
22
27
|
const v = chalk.white(value);
|
|
23
|
-
const t = tag ? chalk.dim(` (${tag})`) : '';
|
|
28
|
+
const t = tag ? chalk.hex(PINK).dim(` (${tag})`) : '';
|
|
24
29
|
return ` ${k}${v}${t}`;
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -47,8 +52,8 @@ export const print = {
|
|
|
47
52
|
export function timeAgo(date) {
|
|
48
53
|
if (!date) return 'never';
|
|
49
54
|
const seconds = Math.floor((Date.now() - new Date(date)) / 1000);
|
|
50
|
-
if (seconds < 10)
|
|
51
|
-
if (seconds < 60)
|
|
55
|
+
if (seconds < 10) return 'just now';
|
|
56
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
52
57
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
53
58
|
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
54
59
|
return `${Math.floor(seconds / 86400)}d ago`;
|
|
@@ -57,7 +62,7 @@ export function timeAgo(date) {
|
|
|
57
62
|
// ─── ms formatting ────────────────────────────────────────────────────────────
|
|
58
63
|
|
|
59
64
|
export function formatMs(ms) {
|
|
60
|
-
if (ms < 1000)
|
|
65
|
+
if (ms < 1000) return `${ms}ms`;
|
|
61
66
|
if (ms < 60000) return `${ms / 1000}s`;
|
|
62
67
|
return `${ms / 60000} min`;
|
|
63
68
|
}
|
package/src/updater.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { GITWRIT_DIR } from './paths.js';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname } from 'path';
|
|
7
|
+
|
|
8
|
+
// ─── current version ──────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const PKG = require(join(dirname(fileURLToPath(import.meta.url)), '../package.json'));
|
|
12
|
+
const CURRENT_VERSION = PKG.version;
|
|
13
|
+
|
|
14
|
+
// ─── cache ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const CACHE_FILE = join(GITWRIT_DIR, 'update-check.json');
|
|
17
|
+
const CACHE_TTL = 1000 * 60 * 60 * 24; // 24 hours
|
|
18
|
+
|
|
19
|
+
async function readCache() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readFile(CACHE_FILE, 'utf8');
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function writeCache(data) {
|
|
29
|
+
try {
|
|
30
|
+
await mkdir(GITWRIT_DIR, { recursive: true });
|
|
31
|
+
await writeFile(CACHE_FILE, JSON.stringify(data), 'utf8');
|
|
32
|
+
} catch {
|
|
33
|
+
// cache write failure is non-fatal
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── registry fetch ───────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
async function fetchLatestVersion() {
|
|
40
|
+
const res = await fetch('https://registry.npmjs.org/gitwrit/latest', {
|
|
41
|
+
signal: AbortSignal.timeout(3000), // 3s timeout — never block the user
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) throw new Error(`registry responded ${res.status}`);
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
return data.version;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── public api ───────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
// returns the latest version string if an update is available,
|
|
51
|
+
// or null if up to date, offline, or the check fails for any reason.
|
|
52
|
+
// always resolves — never rejects.
|
|
53
|
+
export async function checkForUpdate() {
|
|
54
|
+
try {
|
|
55
|
+
const cache = await readCache();
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
|
|
58
|
+
// use cached result if fresh
|
|
59
|
+
if (cache && (now - cache.checkedAt) < CACHE_TTL) {
|
|
60
|
+
return isNewer(cache.latestVersion, CURRENT_VERSION)
|
|
61
|
+
? cache.latestVersion
|
|
62
|
+
: null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// fetch fresh
|
|
66
|
+
const latestVersion = await fetchLatestVersion();
|
|
67
|
+
await writeCache({ latestVersion, checkedAt: now });
|
|
68
|
+
|
|
69
|
+
return isNewer(latestVersion, CURRENT_VERSION) ? latestVersion : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null; // offline, registry down, anything — silent
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function currentVersion() {
|
|
76
|
+
return CURRENT_VERSION;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── semver comparison ────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function isNewer(latest, current) {
|
|
82
|
+
const parse = v => v.replace(/^v/, '').split('.').map(Number);
|
|
83
|
+
const [lMaj, lMin, lPat] = parse(latest);
|
|
84
|
+
const [cMaj, cMin, cPat] = parse(current);
|
|
85
|
+
if (lMaj !== cMaj) return lMaj > cMaj;
|
|
86
|
+
if (lMin !== cMin) return lMin > cMin;
|
|
87
|
+
return lPat > cPat;
|
|
88
|
+
}
|