sdocs-dev 1.3.1 → 1.4.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 (2) hide show
  1. package/bin/sdocs-dev.js +192 -25
  2. package/package.json +1 -1
package/bin/sdocs-dev.js CHANGED
@@ -16,50 +16,208 @@ 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...');
97
+ try {
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;
34
107
  try {
35
- if (Date.now() - fs.statSync(UPDATE_CACHE).mtimeMs < THREE_DAYS) return;
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
+ return;
182
+ }
183
+ console.log('\nSDocs setup: no coding-agent configs detected.');
184
+ const a = await ask('Do you use opencode? [y/N] ');
185
+ const writtenTo = [];
186
+ if (a === 'y' || a === 'yes') {
187
+ const target = path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md');
188
+ try { appendBlockTo(target); writtenTo.push(target); console.log(`\u2713 Wrote SDocs section to ${target}`); }
189
+ catch (e) { console.error(`Failed to write ${target}: ${e.message}`); }
190
+ }
191
+ writeSetupState({ setupCompleted: new Date().toISOString(), writtenTo, declined: writtenTo.length === 0 });
192
+ console.log('Run `sdoc setup` any time to revisit.');
193
+ return;
194
+ }
195
+
196
+ console.log('\nSDocs can teach your coding agents to use it.');
197
+ console.log('Detected: ' + detected.map(t => t.name).join(', '));
198
+ console.log('\nWill append a short SDocs section to:');
199
+ for (const t of detected) console.log(' ' + t.filePath);
200
+ console.log('\n--- block to add ---');
201
+ console.log(AGENT_BLOCK.trim());
202
+ console.log('--- end block ---\n');
203
+
204
+ const a = await ask('Add to all? [Y/n/skip] ');
205
+ const skipped = a === 'skip' || (a && a !== 'y' && a !== 'yes');
206
+ if (skipped) {
207
+ writeSetupState({ setupCompleted: new Date().toISOString(), writtenTo: [], declined: true });
208
+ console.log('Skipped. Run `sdoc setup` any time to revisit.');
209
+ return;
210
+ }
211
+
212
+ const writtenTo = [];
213
+ for (const t of detected) {
214
+ try { appendBlockTo(t.filePath); writtenTo.push(t.filePath); console.log(`\u2713 ${t.name}: ${t.filePath}`); }
215
+ catch (e) { console.error(`\u2717 ${t.name}: ${e.message}`); }
216
+ }
217
+ writeSetupState({ setupCompleted: new Date().toISOString(), writtenTo, declined: false });
218
+ console.log('Done. Run `sdoc setup` any time to revisit.');
219
+ }
220
+
63
221
  // ── Help ───────────────────────────────────────────────────
64
222
  const HELP = `
65
223
  SDocs CLI
@@ -78,6 +236,7 @@ USAGE
78
236
  sdoc charts Chart types, options, and styling guide
79
237
  sdoc defaults Show ~/.sdocs/styles.yaml
80
238
  sdoc defaults --reset Remove default styles
239
+ sdoc setup Wire SDocs into your coding agents
81
240
  sdoc help Show this help
82
241
  cat file.md | sdoc Pipe markdown from stdin
83
242
  cat file.md | sdoc share Pipe to clipboard link
@@ -209,7 +368,7 @@ BLOCKS (shared styling for code, blockquote, and chart blocks)
209
368
  color string Text color for all block types. Cascades to code,
210
369
  blockquote, and chart text unless overridden.
211
370
 
212
- CHARTS (see also: \`sdoc charts\` for the full chart reference)
371
+ CHARTS
213
372
  chart:
214
373
  accent string Palette base color (hex). Default: "#3b82f6"
215
374
  palette string Palette mode. Default: "monochrome"
@@ -218,6 +377,9 @@ CHARTS (see also: \`sdoc charts\` for the full chart reference)
218
377
  background string Chart background. Default: inherits blocks.background
219
378
  textColor string Chart labels/axes. Default: inherits blocks.color
220
379
 
380
+ Run \`sdoc charts\` for the full chart reference — chart types, JSON
381
+ format, axis/legend/annotation options, and per-chart styling overrides.
382
+
221
383
  COLOR CASCADE
222
384
  Colors cascade from general → specific:
223
385
  color → headers.color → h1.color, h2.color, h3.color, h4.color
@@ -499,7 +661,7 @@ var slugify = require('../public/sdocs-slugify').slugify;
499
661
 
500
662
  // ── Parse args ────────────────────────────────────────────
501
663
 
502
- const SUBCOMMANDS = new Set(['new', 'share', 'schema', 'defaults', 'help', 'charts']);
664
+ const SUBCOMMANDS = new Set(['new', 'share', 'schema', 'defaults', 'help', 'charts', 'setup']);
503
665
 
504
666
  function parseArgs(argv) {
505
667
  const args = argv || process.argv.slice(2);
@@ -729,6 +891,7 @@ if (require.main === module) {
729
891
  if (opts.subcommand === 'help') { console.log(HELP); process.exit(0); }
730
892
  if (opts.subcommand === 'schema') { console.log(SCHEMA); process.exit(0); }
731
893
  if (opts.subcommand === 'charts') { console.log(CHARTS_HELP); process.exit(0); }
894
+ if (opts.subcommand === 'setup') { await runSetup({ force: true }); process.exit(0); }
732
895
  if (opts.subcommand === 'defaults') {
733
896
  if (opts.resetFlag) resetDefaults();
734
897
  else showDefaults();
@@ -796,14 +959,18 @@ if (require.main === module) {
796
959
  } catch (_) {
797
960
  process.stdout.write(url + '\n');
798
961
  }
799
- checkForUpdate();
962
+ refreshUpdateCache();
963
+ await promptUpdateIfAvailable();
964
+ await runSetup();
800
965
  return;
801
966
  }
802
967
 
803
968
  // Default: open browser
804
969
  openBrowser(url);
805
970
  console.log(`SDocs → ${url.length > 80 ? url.slice(0, 77) + '...' : url}`);
806
- checkForUpdate();
971
+ refreshUpdateCache();
972
+ await promptUpdateIfAvailable();
973
+ await runSetup();
807
974
  })().catch(e => {
808
975
  console.error('sdoc:', e.message);
809
976
  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.0",
4
4
  "description": "Open, share, and style markdown files from the terminal",
5
5
  "main": "bin/sdocs-dev.js",
6
6
  "bin": {