sdocs-dev 1.3.1 → 1.4.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/bin/sdocs-dev.js +200 -25
- package/package.json +1 -1
package/bin/sdocs-dev.js
CHANGED
|
@@ -16,50 +16,216 @@ const { execSync } = require('child_process');
|
|
|
16
16
|
const SDocYaml = require('../public/sdocs-yaml.js');
|
|
17
17
|
const SDocStyles = require('../public/sdocs-styles.js');
|
|
18
18
|
|
|
19
|
-
const https
|
|
20
|
-
const os
|
|
19
|
+
const https = require('https');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const readline = require('readline');
|
|
21
22
|
|
|
22
23
|
const DEFAULT_URL = 'https://sdocs.dev';
|
|
23
24
|
const VERSION = require('../package.json').version;
|
|
24
25
|
|
|
25
|
-
// ── Update check
|
|
26
|
+
// ── Update check ─────────────────────────────────────────
|
|
27
|
+
// Daily background refresh of the latest version from npm (silent).
|
|
28
|
+
// On next invocation, if the cached `latest` is newer, prompt the user
|
|
29
|
+
// to install. Y/y runs `npm i -g sdocs-dev` and re-execs their command.
|
|
26
30
|
|
|
27
|
-
const UPDATE_CACHE = path.join(os.homedir(), '.
|
|
28
|
-
const
|
|
31
|
+
const UPDATE_CACHE = path.join(os.homedir(), '.sdocs', 'update-check.json');
|
|
32
|
+
const SETUP_CACHE = path.join(os.homedir(), '.sdocs', 'setup.json');
|
|
33
|
+
const ONE_DAY = 86400000;
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
// ── Agent integration block ──────────────────────────────
|
|
36
|
+
// IMPORTANT: keep this in sync with the per-agent setup snippets in
|
|
37
|
+
// public/sdoc.md (the "Set up your agent" section). If you reword one,
|
|
38
|
+
// reword the other. The marker comment on line 1 is used to detect
|
|
39
|
+
// whether the block has already been written to a file (idempotent re-runs).
|
|
40
|
+
const AGENT_BLOCK = `<!-- sdocs-agent-block -->
|
|
41
|
+
## SDocs
|
|
42
|
+
|
|
43
|
+
SDocs (sdocs.dev) renders markdown with clean styling you can adjust.
|
|
44
|
+
Content lives in the URL hash — nothing hits a server.
|
|
45
|
+
|
|
46
|
+
Use it (or offer it) when the user wants to read, share, or export a \`.md\` file.
|
|
47
|
+
|
|
48
|
+
The \`sdoc\` CLI is installed globally:
|
|
49
|
+
- \`sdoc file.md\` — open styled in browser (great for easy reading).
|
|
50
|
+
- \`sdoc share file.md\` — copy shareable URL to clipboard.
|
|
51
|
+
- \`sdoc schema\` — how to adjust all stylable properties (fonts, colors, spacing).
|
|
52
|
+
- \`sdoc charts\` — chart types, JSON format, and styling. Charts render via Chart.js from \`\`\`chart code blocks.
|
|
53
|
+
- \`sdoc --help\` — full usage.
|
|
54
|
+
|
|
55
|
+
Source: https://github.com/JoshInLisbon/SDocs
|
|
56
|
+
`;
|
|
57
|
+
const AGENT_BLOCK_MARKER = '<!-- sdocs-agent-block -->';
|
|
58
|
+
|
|
59
|
+
const AGENT_TARGETS = [
|
|
60
|
+
{ name: 'Claude Code', dir: '.claude', file: 'CLAUDE.md' },
|
|
61
|
+
{ name: 'Codex', dir: '.codex', file: 'AGENTS.md' },
|
|
62
|
+
{ name: 'Gemini CLI', dir: '.gemini', file: 'GEMINI.md' },
|
|
63
|
+
{ name: 'opencode', dir: path.join('.config', 'opencode'), file: 'AGENTS.md' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function isNewer(latest, current) {
|
|
67
|
+
const a = latest.split('.').map(Number);
|
|
68
|
+
const b = current.split('.').map(Number);
|
|
69
|
+
for (let i = 0; i < 3; i++) {
|
|
70
|
+
if (a[i] > b[i]) return true;
|
|
71
|
+
if (a[i] < b[i]) return false;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readCachedLatest() {
|
|
77
|
+
try { return JSON.parse(fs.readFileSync(UPDATE_CACHE, 'utf-8')).latest; }
|
|
78
|
+
catch (_) { return null; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function promptUpdateIfAvailable() {
|
|
82
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
83
|
+
if (process.env.NO_UPDATE_NOTIFIER || process.env.CI) return;
|
|
84
|
+
|
|
85
|
+
const latest = readCachedLatest();
|
|
86
|
+
if (!latest || !isNewer(latest, VERSION)) return;
|
|
87
|
+
|
|
88
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
89
|
+
const answer = await new Promise(resolve => {
|
|
90
|
+
rl.question(`\nUpdate available: ${VERSION} \u2192 ${latest}. Install now? [Y/n] `, a => {
|
|
91
|
+
rl.close(); resolve(a.trim().toLowerCase());
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
if (answer && answer !== 'y' && answer !== 'yes') return;
|
|
32
95
|
|
|
33
|
-
|
|
96
|
+
console.log('Installing sdocs-dev@latest...');
|
|
34
97
|
try {
|
|
35
|
-
|
|
98
|
+
execSync('npm i -g sdocs-dev@latest', { stdio: 'inherit' });
|
|
99
|
+
console.log(`\u2713 Updated to v${latest}`);
|
|
100
|
+
} catch (_) {
|
|
101
|
+
console.error('Update failed. You may need: sudo npm i -g sdocs-dev');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function refreshUpdateCache() {
|
|
106
|
+
if (!process.stdout.isTTY || process.env.NO_UPDATE_NOTIFIER || process.env.CI) return;
|
|
107
|
+
try {
|
|
108
|
+
if (Date.now() - fs.statSync(UPDATE_CACHE).mtimeMs < ONE_DAY) return;
|
|
36
109
|
} catch (_) {}
|
|
37
110
|
|
|
38
|
-
console.log('Checking for updates...');
|
|
39
111
|
https.get('https://registry.npmjs.org/-/package/sdocs-dev/dist-tags', { timeout: 3000 }, res => {
|
|
40
112
|
let data = '';
|
|
41
113
|
res.on('data', chunk => { data += chunk; });
|
|
42
114
|
res.on('end', () => {
|
|
43
115
|
try {
|
|
44
116
|
const latest = JSON.parse(data).latest;
|
|
45
|
-
// Update cache timestamp
|
|
46
117
|
fs.mkdirSync(path.dirname(UPDATE_CACHE), { recursive: true });
|
|
47
118
|
fs.writeFileSync(UPDATE_CACHE, JSON.stringify({ latest }));
|
|
48
|
-
|
|
49
|
-
const a = latest.split('.').map(Number);
|
|
50
|
-
const b = VERSION.split('.').map(Number);
|
|
51
|
-
let newer = false;
|
|
52
|
-
for (let i = 0; i < 3; i++) { if (a[i] > b[i]) { newer = true; break; } if (a[i] < b[i]) break; }
|
|
53
|
-
if (newer) {
|
|
54
|
-
console.log(`Update available: ${VERSION} \u2192 ${latest} \u2014 run \`npm i -g sdocs-dev\` to update`);
|
|
55
|
-
} else {
|
|
56
|
-
console.log(`Up to date (v${VERSION})`);
|
|
57
|
-
}
|
|
58
119
|
} catch (_) {}
|
|
59
120
|
});
|
|
60
121
|
}).on('error', () => {}).on('timeout', function () { this.destroy(); });
|
|
61
122
|
}
|
|
62
123
|
|
|
124
|
+
// ── Agent setup ──────────────────────────────────────────
|
|
125
|
+
// On first interactive run, detect which coding-agent config dirs exist
|
|
126
|
+
// and offer to append AGENT_BLOCK to each. Tracked in ~/.sdocs/setup.json
|
|
127
|
+
// so we never prompt twice. Manually re-runnable via `sdoc setup`.
|
|
128
|
+
|
|
129
|
+
function readSetupState() {
|
|
130
|
+
try { return JSON.parse(fs.readFileSync(SETUP_CACHE, 'utf-8')); }
|
|
131
|
+
catch (_) { return null; }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeSetupState(state) {
|
|
135
|
+
try {
|
|
136
|
+
fs.mkdirSync(path.dirname(SETUP_CACHE), { recursive: true });
|
|
137
|
+
fs.writeFileSync(SETUP_CACHE, JSON.stringify(state, null, 2));
|
|
138
|
+
} catch (_) {}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function detectAgents() {
|
|
142
|
+
const home = os.homedir();
|
|
143
|
+
return AGENT_TARGETS
|
|
144
|
+
.map(t => ({ ...t, dirPath: path.join(home, t.dir), filePath: path.join(home, t.dir, t.file) }))
|
|
145
|
+
.filter(t => fs.existsSync(t.dirPath));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function fileHasBlock(filePath) {
|
|
149
|
+
try { return fs.readFileSync(filePath, 'utf-8').includes(AGENT_BLOCK_MARKER); }
|
|
150
|
+
catch (_) { return false; }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function appendBlockTo(filePath) {
|
|
154
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
155
|
+
const exists = fs.existsSync(filePath);
|
|
156
|
+
const prefix = exists && fs.readFileSync(filePath, 'utf-8').endsWith('\n') ? '\n' : (exists ? '\n\n' : '');
|
|
157
|
+
fs.appendFileSync(filePath, prefix + AGENT_BLOCK);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function ask(question) {
|
|
161
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
162
|
+
return new Promise(resolve => {
|
|
163
|
+
rl.question(question, a => { rl.close(); resolve(a.trim().toLowerCase()); });
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function runSetup({ force = false } = {}) {
|
|
168
|
+
if (!force) {
|
|
169
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
170
|
+
if (process.env.CI || process.env.SDOCS_NO_SETUP) return;
|
|
171
|
+
if (readSetupState()) return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const detected = detectAgents().filter(t => !fileHasBlock(t.filePath));
|
|
175
|
+
|
|
176
|
+
if (detected.length === 0) {
|
|
177
|
+
// Fallback: ask about opencode if nothing detected and not already set up
|
|
178
|
+
const opencodeAlreadyDone = fileHasBlock(path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md'));
|
|
179
|
+
if (opencodeAlreadyDone) {
|
|
180
|
+
writeSetupState({ setupCompleted: new Date().toISOString(), writtenTo: [], declined: false });
|
|
181
|
+
console.log('\nSDocs is already set up in all detected agent configs. Nothing to do.');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
console.log('\n\u2728\u2500\u2500\u2500\u2500\u2500\u2500\u2500 SDocs setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2728');
|
|
185
|
+
console.log('First run only \u2014 wire SDocs into your coding agents.\n');
|
|
186
|
+
console.log('No coding-agent configs detected.');
|
|
187
|
+
const a = await ask('Do you use opencode? [y/N] ');
|
|
188
|
+
const writtenTo = [];
|
|
189
|
+
if (a === 'y' || a === 'yes') {
|
|
190
|
+
const target = path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md');
|
|
191
|
+
try { appendBlockTo(target); writtenTo.push(target); console.log(`\u2713 Wrote SDocs section to ${target}`); }
|
|
192
|
+
catch (e) { console.error(`Failed to write ${target}: ${e.message}`); }
|
|
193
|
+
console.log('Done. Run `sdoc setup` any time to revisit.');
|
|
194
|
+
} else {
|
|
195
|
+
console.log('Skipped. Run `sdoc setup` any time to revisit.');
|
|
196
|
+
}
|
|
197
|
+
writeSetupState({ setupCompleted: new Date().toISOString(), writtenTo, declined: writtenTo.length === 0 });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log('\n\u2728\u2500\u2500\u2500\u2500\u2500\u2500\u2500 SDocs setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2728');
|
|
202
|
+
console.log('First run only \u2014 wire SDocs into your coding agents.\n');
|
|
203
|
+
console.log('Detected: ' + detected.map(t => t.name).join(', '));
|
|
204
|
+
console.log('\nWill append a short SDocs section to:');
|
|
205
|
+
for (const t of detected) console.log(' ' + t.filePath);
|
|
206
|
+
const RULE = '\u2550'.repeat(36);
|
|
207
|
+
const previewBody = AGENT_BLOCK.replace(AGENT_BLOCK_MARKER + '\n', '').trim();
|
|
208
|
+
console.log(`\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 Block to add \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
|
|
209
|
+
console.log(previewBody);
|
|
210
|
+
console.log(RULE + '\n');
|
|
211
|
+
|
|
212
|
+
const a = await ask('Add to all? [Y/n/skip] ');
|
|
213
|
+
const skipped = a === 'skip' || (a && a !== 'y' && a !== 'yes');
|
|
214
|
+
if (skipped) {
|
|
215
|
+
writeSetupState({ setupCompleted: new Date().toISOString(), writtenTo: [], declined: true });
|
|
216
|
+
console.log('Skipped. Run `sdoc setup` any time to revisit.');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const writtenTo = [];
|
|
221
|
+
for (const t of detected) {
|
|
222
|
+
try { appendBlockTo(t.filePath); writtenTo.push(t.filePath); console.log(`\u2713 ${t.name}: ${t.filePath}`); }
|
|
223
|
+
catch (e) { console.error(`\u2717 ${t.name}: ${e.message}`); }
|
|
224
|
+
}
|
|
225
|
+
writeSetupState({ setupCompleted: new Date().toISOString(), writtenTo, declined: false });
|
|
226
|
+
console.log('Done. Run `sdoc setup` any time to revisit.');
|
|
227
|
+
}
|
|
228
|
+
|
|
63
229
|
// ── Help ───────────────────────────────────────────────────
|
|
64
230
|
const HELP = `
|
|
65
231
|
SDocs CLI
|
|
@@ -78,6 +244,7 @@ USAGE
|
|
|
78
244
|
sdoc charts Chart types, options, and styling guide
|
|
79
245
|
sdoc defaults Show ~/.sdocs/styles.yaml
|
|
80
246
|
sdoc defaults --reset Remove default styles
|
|
247
|
+
sdoc setup Wire SDocs into your coding agents
|
|
81
248
|
sdoc help Show this help
|
|
82
249
|
cat file.md | sdoc Pipe markdown from stdin
|
|
83
250
|
cat file.md | sdoc share Pipe to clipboard link
|
|
@@ -209,7 +376,7 @@ BLOCKS (shared styling for code, blockquote, and chart blocks)
|
|
|
209
376
|
color string Text color for all block types. Cascades to code,
|
|
210
377
|
blockquote, and chart text unless overridden.
|
|
211
378
|
|
|
212
|
-
CHARTS
|
|
379
|
+
CHARTS
|
|
213
380
|
chart:
|
|
214
381
|
accent string Palette base color (hex). Default: "#3b82f6"
|
|
215
382
|
palette string Palette mode. Default: "monochrome"
|
|
@@ -218,6 +385,9 @@ CHARTS (see also: \`sdoc charts\` for the full chart reference)
|
|
|
218
385
|
background string Chart background. Default: inherits blocks.background
|
|
219
386
|
textColor string Chart labels/axes. Default: inherits blocks.color
|
|
220
387
|
|
|
388
|
+
Run \`sdoc charts\` for the full chart reference — chart types, JSON
|
|
389
|
+
format, axis/legend/annotation options, and per-chart styling overrides.
|
|
390
|
+
|
|
221
391
|
COLOR CASCADE
|
|
222
392
|
Colors cascade from general → specific:
|
|
223
393
|
color → headers.color → h1.color, h2.color, h3.color, h4.color
|
|
@@ -499,7 +669,7 @@ var slugify = require('../public/sdocs-slugify').slugify;
|
|
|
499
669
|
|
|
500
670
|
// ── Parse args ────────────────────────────────────────────
|
|
501
671
|
|
|
502
|
-
const SUBCOMMANDS = new Set(['new', 'share', 'schema', 'defaults', 'help', 'charts']);
|
|
672
|
+
const SUBCOMMANDS = new Set(['new', 'share', 'schema', 'defaults', 'help', 'charts', 'setup']);
|
|
503
673
|
|
|
504
674
|
function parseArgs(argv) {
|
|
505
675
|
const args = argv || process.argv.slice(2);
|
|
@@ -729,6 +899,7 @@ if (require.main === module) {
|
|
|
729
899
|
if (opts.subcommand === 'help') { console.log(HELP); process.exit(0); }
|
|
730
900
|
if (opts.subcommand === 'schema') { console.log(SCHEMA); process.exit(0); }
|
|
731
901
|
if (opts.subcommand === 'charts') { console.log(CHARTS_HELP); process.exit(0); }
|
|
902
|
+
if (opts.subcommand === 'setup') { await runSetup({ force: true }); process.exit(0); }
|
|
732
903
|
if (opts.subcommand === 'defaults') {
|
|
733
904
|
if (opts.resetFlag) resetDefaults();
|
|
734
905
|
else showDefaults();
|
|
@@ -796,14 +967,18 @@ if (require.main === module) {
|
|
|
796
967
|
} catch (_) {
|
|
797
968
|
process.stdout.write(url + '\n');
|
|
798
969
|
}
|
|
799
|
-
|
|
970
|
+
refreshUpdateCache();
|
|
971
|
+
await promptUpdateIfAvailable();
|
|
972
|
+
await runSetup();
|
|
800
973
|
return;
|
|
801
974
|
}
|
|
802
975
|
|
|
803
976
|
// Default: open browser
|
|
804
977
|
openBrowser(url);
|
|
805
978
|
console.log(`SDocs → ${url.length > 80 ? url.slice(0, 77) + '...' : url}`);
|
|
806
|
-
|
|
979
|
+
refreshUpdateCache();
|
|
980
|
+
await promptUpdateIfAvailable();
|
|
981
|
+
await runSetup();
|
|
807
982
|
})().catch(e => {
|
|
808
983
|
console.error('sdoc:', e.message);
|
|
809
984
|
process.exit(1);
|