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.
Files changed (2) hide show
  1. package/bin/sdocs-dev.js +200 -25
  2. 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 = require('https');
20
- const os = require('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 (cached, every 3 days) ──────────────────
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(), '.config', 'sdocs-dev', 'update-check.json');
28
- const THREE_DAYS = 3 * 86400000;
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
- function checkForUpdate() {
31
- if (!process.stdout.isTTY || process.env.NO_UPDATE_NOTIFIER || process.env.CI) return;
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
- // Skip if checked recently
96
+ console.log('Installing sdocs-dev@latest...');
34
97
  try {
35
- if (Date.now() - fs.statSync(UPDATE_CACHE).mtimeMs < THREE_DAYS) return;
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 (see also: \`sdoc charts\` for the full chart reference)
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
- checkForUpdate();
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
- checkForUpdate();
979
+ refreshUpdateCache();
980
+ await promptUpdateIfAvailable();
981
+ await runSetup();
807
982
  })().catch(e => {
808
983
  console.error('sdoc:', e.message);
809
984
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdocs-dev",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "Open, share, and style markdown files from the terminal",
5
5
  "main": "bin/sdocs-dev.js",
6
6
  "bin": {