sdocs-dev 1.6.2 → 1.12.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/lib/io.js ADDED
@@ -0,0 +1,173 @@
1
+ // CLI I/O helpers: argv parsing, content reading, browser opening.
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execFileSync } = require('child_process');
6
+ const { transcludeCells, isWrappedFile, wrapForDisplay } = require('./cells-transclude');
7
+
8
+ const SUBCOMMANDS = new Set([
9
+ 'new', 'share', 'schema', 'defaults', 'help', 'version',
10
+ 'charts', 'diagrams', 'cells', 'comments',
11
+ 'setup', 'safe', 'auto-update', 'refresh', 'upgrade',
12
+ 'bridge', 'feedback',
13
+ 'slides', 'present',
14
+ 'library',
15
+ 'color-analysis',
16
+ ]);
17
+
18
+ // CLI tag arguments are `+tag` (shell-safe, no quoting). Tags written
19
+ // this way are injected into the file's YAML front matter at open time;
20
+ // front matter is the only place SDocs stores tags.
21
+ const TAG_ARG = /^\+[A-Za-z][\w-]{0,63}$/;
22
+
23
+ function parseArgs(argv) {
24
+ const args = argv || process.argv.slice(2);
25
+ let file = null;
26
+ let extra = null;
27
+ let mode = null;
28
+ let url = null;
29
+ let subcommand = null;
30
+ let section = null;
31
+ let theme = null;
32
+ let resetFlag = false;
33
+ let shortFlag = false;
34
+ let jsonFlag = false;
35
+ let auditFlag = false;
36
+ let waitFlag = false;
37
+ let messageText = null;
38
+ let connectTimeoutS = null;
39
+ let idleTimeoutS = null;
40
+ let reconnectGraceMs = null;
41
+ let keepOpenFlag = false;
42
+ let logFile = null;
43
+ let tagsFlag = false;
44
+ let helpFlag = false;
45
+ let yesFlag = false;
46
+ const addTags = [];
47
+
48
+ for (let i = 0; i < args.length; i++) {
49
+ const arg = args[i];
50
+
51
+ // `--help` before a subcommand prints the global help. After a
52
+ // subcommand, it is a flag the subcommand handler reads (library
53
+ // uses this to print its own help).
54
+ if (arg === '--help' || arg === '-h') {
55
+ if (subcommand) helpFlag = true; else subcommand = 'help';
56
+ continue;
57
+ }
58
+ if (arg === '--schema') { subcommand = 'schema'; continue; }
59
+ if (arg === '--version' || arg === '-v' || arg === '-V') { subcommand = 'version'; continue; }
60
+
61
+ if (arg === '--write') { mode = 'write'; continue; }
62
+ if (arg === '--style') { mode = 'style'; continue; }
63
+ if (arg === '--raw') { mode = 'raw'; continue; }
64
+ if (arg === '--read') { mode = 'read'; continue; }
65
+ if (arg === '--comment') { mode = 'comment'; continue; }
66
+ if (arg === '--light') { theme = 'light'; continue; }
67
+ if (arg === '--dark') { theme = 'dark'; continue; }
68
+
69
+ if (arg === '--mode' || arg === '-m') {
70
+ mode = args[++i];
71
+ if (!['read', 'write', 'style', 'raw', 'comment'].includes(mode)) {
72
+ console.error(`sdoc: unknown mode "${mode}" — use read, write, style, raw, or comment`);
73
+ process.exit(1);
74
+ }
75
+ continue;
76
+ }
77
+
78
+ if (arg === '--url') { url = args[++i]; continue; }
79
+ if (arg === '--section' || arg === '-s') { section = args[++i]; continue; }
80
+ if (arg === '--reset') { resetFlag = true; continue; }
81
+ if (arg === '--short') { shortFlag = true; continue; }
82
+ if (arg === '--json') { jsonFlag = true; continue; }
83
+ if (arg === '--audit') { auditFlag = true; continue; }
84
+ if (arg === '--wait') { waitFlag = true; continue; }
85
+
86
+ // Note: `--mode` already owns `-m` for editor-mode selection, so the
87
+ // bridge message flag is `--message` with no short alias.
88
+ if (arg === '--message') { messageText = args[++i]; continue; }
89
+ if (arg === '--connect-timeout') { connectTimeoutS = Number(args[++i]); continue; }
90
+ if (arg === '--idle-timeout') { idleTimeoutS = Number(args[++i]); continue; }
91
+ if (arg === '--reconnect-grace') { reconnectGraceMs = Number(args[++i]); continue; }
92
+ if (arg === '--keep-open') { keepOpenFlag = true; continue; }
93
+ if (arg === '--log-file') { logFile = args[++i]; continue; }
94
+ if (arg === '--tags') { tagsFlag = true; continue; }
95
+ if (arg === '--yes' || arg === '-y') { yesFlag = true; continue; }
96
+
97
+ if (!subcommand && SUBCOMMANDS.has(arg)) {
98
+ subcommand = arg;
99
+ continue;
100
+ }
101
+
102
+ // Tag arguments anywhere on the command line: collected into
103
+ // addTags, used by the library tap to inject tags into the file's
104
+ // front matter at open time.
105
+ if (TAG_ARG.test(arg)) { addTags.push(arg.slice(1).toLowerCase()); continue; }
106
+
107
+ if (!file) { file = arg; continue; }
108
+ // Second positional is captured as `extra` so `sdoc slides icons heart`
109
+ // gets {subcommand: 'slides', file: 'icons', extra: 'heart'}.
110
+ if (extra === null) { extra = arg; continue; }
111
+ }
112
+
113
+ return {
114
+ file, extra, mode, url, subcommand, section, theme,
115
+ resetFlag, shortFlag, jsonFlag, auditFlag, waitFlag,
116
+ messageText, connectTimeoutS, idleTimeoutS, reconnectGraceMs,
117
+ keepOpenFlag, logFile,
118
+ tagsFlag, helpFlag, yesFlag,
119
+ addTags,
120
+ };
121
+ }
122
+
123
+ async function readContent(file) {
124
+ if (file) {
125
+ const resolved = path.resolve(file);
126
+ if (!fs.existsSync(resolved)) {
127
+ console.error(`sdoc: file not found: ${file}`);
128
+ process.exit(1);
129
+ }
130
+ let raw = fs.readFileSync(resolved, 'utf-8');
131
+ // .csv / .mmd / .mermaid files are wrapped in their fenced block so the
132
+ // renderer picks them up (a standalone .csv opens directly as a sheet).
133
+ // The same transform runs in the bridge for live sessions - if you change
134
+ // one, change the other (both call wrapForDisplay).
135
+ if (isWrappedFile(file)) {
136
+ raw = wrapForDisplay(raw, file);
137
+ } else {
138
+ // Bake any {{path/to/file.csv}} cells references into the doc, resolving
139
+ // paths relative to the markdown file. Self-contained docs share safely.
140
+ raw = transcludeCells(raw, path.dirname(resolved));
141
+ }
142
+ return raw;
143
+ }
144
+
145
+ if (!process.stdin.isTTY) {
146
+ return new Promise((resolve, reject) => {
147
+ let data = '';
148
+ process.stdin.setEncoding('utf-8');
149
+ process.stdin.on('data', chunk => data += chunk);
150
+ process.stdin.on('end', () => resolve(data));
151
+ process.stdin.on('error', reject);
152
+ });
153
+ }
154
+
155
+ return null; // no content — just open studio
156
+ }
157
+
158
+ function openBrowser(url) {
159
+ try {
160
+ if (process.platform === 'darwin') execFileSync('open', [url]);
161
+ else if (process.platform === 'win32') execFileSync('cmd', ['/c', 'start', '', url]);
162
+ else execFileSync('xdg-open', [url]);
163
+ } catch {
164
+ console.log(`Open in browser: ${url}`);
165
+ }
166
+ }
167
+
168
+ module.exports = {
169
+ SUBCOMMANDS,
170
+ parseArgs,
171
+ readContent,
172
+ openBrowser,
173
+ };
@@ -0,0 +1,145 @@
1
+ // Optional, opt-in autostart for the library agent.
2
+ //
3
+ // macOS: writes a LaunchAgent plist to ~/Library/LaunchAgents/ and
4
+ // loads it via launchctl. Linux/Windows: not yet implemented; the
5
+ // CLI prints "not supported on your platform" and exits.
6
+ //
7
+ // The plist points at the current Node binary plus the resolved CLI
8
+ // script path, so the OS can spawn the agent on login without relying
9
+ // on PATH or shell init.
10
+
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const { execFileSync } = require('child_process');
15
+
16
+ const LABEL = 'dev.sdocs.library';
17
+
18
+ function launchAgentsDir() {
19
+ if (process.env.SDOCS_LAUNCHAGENTS_DIR) return process.env.SDOCS_LAUNCHAGENTS_DIR;
20
+ return path.join(os.homedir(), 'Library', 'LaunchAgents');
21
+ }
22
+
23
+ function plistPath() {
24
+ return path.join(launchAgentsDir(), LABEL + '.plist');
25
+ }
26
+
27
+ function logPath() {
28
+ if (process.env.SDOCS_HOME) return path.join(process.env.SDOCS_HOME, 'library-autostart.log');
29
+ return path.join(os.homedir(), '.sdocs', 'library-autostart.log');
30
+ }
31
+
32
+ function isSupported() {
33
+ return process.platform === 'darwin';
34
+ }
35
+
36
+ function isEnabled() {
37
+ if (!isSupported()) return false;
38
+ return fs.existsSync(plistPath());
39
+ }
40
+
41
+ // Pure: builds the plist XML from the given paths. Exported for tests.
42
+ function buildPlist({ nodePath, scriptPath, logPath: lp }) {
43
+ const escape = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
44
+ return `<?xml version="1.0" encoding="UTF-8"?>
45
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
46
+ <plist version="1.0">
47
+ <dict>
48
+ <key>Label</key>
49
+ <string>${LABEL}</string>
50
+ <key>ProgramArguments</key>
51
+ <array>
52
+ <string>${escape(nodePath)}</string>
53
+ <string>${escape(scriptPath)}</string>
54
+ <string>library</string>
55
+ </array>
56
+ <key>RunAtLoad</key>
57
+ <true/>
58
+ <key>KeepAlive</key>
59
+ <dict>
60
+ <key>SuccessfulExit</key>
61
+ <false/>
62
+ </dict>
63
+ <key>ThrottleInterval</key>
64
+ <integer>10</integer>
65
+ <key>StandardOutPath</key>
66
+ <string>${escape(lp)}</string>
67
+ <key>StandardErrorPath</key>
68
+ <string>${escape(lp)}</string>
69
+ </dict>
70
+ </plist>
71
+ `;
72
+ }
73
+
74
+ // Resolve the CLI script the OS should run. process.argv[1] is the JS
75
+ // entrypoint the user invoked. Falls back to a path relative to this
76
+ // module if argv[1] isn't usable (e.g. test runners).
77
+ function resolveScriptPath() {
78
+ const candidate = process.argv[1];
79
+ if (candidate && fs.existsSync(candidate)) return path.resolve(candidate);
80
+ return path.resolve(__dirname, '..', 'bin', 'sdocs-dev.js');
81
+ }
82
+
83
+ function writePlist() {
84
+ fs.mkdirSync(launchAgentsDir(), { recursive: true });
85
+ const content = buildPlist({
86
+ nodePath: process.execPath,
87
+ scriptPath: resolveScriptPath(),
88
+ logPath: logPath(),
89
+ });
90
+ fs.writeFileSync(plistPath(), content);
91
+ return plistPath();
92
+ }
93
+
94
+ function removePlist() {
95
+ try { fs.unlinkSync(plistPath()); } catch (_) {}
96
+ }
97
+
98
+ function launchctl(verb) {
99
+ if (process.env.SDOCS_AUTOSTART_DRY_RUN === '1') return { ok: true, dryRun: true };
100
+ try {
101
+ execFileSync('launchctl', [verb, plistPath()], { stdio: 'pipe' });
102
+ return { ok: true };
103
+ } catch (e) {
104
+ return { ok: false, message: (e.stderr ? e.stderr.toString() : e.message).trim() };
105
+ }
106
+ }
107
+
108
+ function enable() {
109
+ if (!isSupported()) {
110
+ return { ok: false, message: 'autostart is only supported on macOS for now.' };
111
+ }
112
+ const wrote = writePlist();
113
+ // Unload any prior copy so reload picks up the fresh plist if node path changed.
114
+ launchctl('unload');
115
+ const r = launchctl('load');
116
+ if (!r.ok) {
117
+ return { ok: false, message: 'wrote plist but launchctl load failed: ' + r.message };
118
+ }
119
+ return { ok: true, path: wrote };
120
+ }
121
+
122
+ function disable() {
123
+ if (!isSupported()) {
124
+ return { ok: false, message: 'autostart is only supported on macOS for now.' };
125
+ }
126
+ if (!isEnabled()) return { ok: true, alreadyDisabled: true };
127
+ launchctl('unload');
128
+ removePlist();
129
+ return { ok: true };
130
+ }
131
+
132
+ function status() {
133
+ return {
134
+ supported: isSupported(),
135
+ enabled: isEnabled(),
136
+ plistPath: plistPath(),
137
+ };
138
+ }
139
+
140
+ module.exports = {
141
+ LABEL,
142
+ isSupported, isEnabled,
143
+ enable, disable, status,
144
+ buildPlist, plistPath, logPath, resolveScriptPath,
145
+ };
@@ -0,0 +1,307 @@
1
+ // CLI handlers for the `sdoc library ...` verbs and the on-open
2
+ // indexing tap used by the default `sdoc <file>` command.
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const store = require('./library-store');
8
+ const libIndex = require('./library-index');
9
+ const libServer = require('./library-server');
10
+ const autostart = require('./library-autostart');
11
+ const helpText = require('./help-text');
12
+ const { openBrowser } = require('./io');
13
+ const { DEFAULT_URL } = require('./constants');
14
+ const http = require('http');
15
+
16
+ function libraryEnable() {
17
+ const s = store.loadState();
18
+ s.enabled = true;
19
+ store.saveState(s);
20
+ console.log('library: enabled');
21
+ }
22
+
23
+ function libraryDisable() {
24
+ const s = store.loadState();
25
+ s.enabled = false;
26
+ store.saveState(s);
27
+ console.log('library: disabled (existing index left in place)');
28
+ }
29
+
30
+ function libraryStatus() {
31
+ const s = store.loadState();
32
+ const idx = store.loadIndex();
33
+ const last = s.lastScanAt ? new Date(s.lastScanAt).toISOString() : 'never';
34
+ console.log(`library: ${s.enabled === false ? 'disabled' : 'enabled'}`);
35
+ console.log(`entries: ${idx.entries.length}`);
36
+ console.log(`last scan: ${last}`);
37
+ }
38
+
39
+ function libraryRebuild() {
40
+ console.log('library: rebuilding...');
41
+ const result = libIndex.rebuild();
42
+ console.log(`library: scanned ${result.scanned}, added ${result.added}, updated ${result.updated}`);
43
+ }
44
+
45
+ // Walk up from a directory looking for `.git/`. Falls back to the start
46
+ // directory if no repo root is found. Matches the rule the agent's
47
+ // /api/library/project-tags endpoint uses, so CLI output is consistent
48
+ // with what the browser shows.
49
+ function resolveProjectRoot(startDir) {
50
+ let dir = path.resolve(startDir);
51
+ for (let i = 0; i < 30; i++) {
52
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
53
+ const parent = path.dirname(dir);
54
+ if (parent === dir) break;
55
+ dir = parent;
56
+ }
57
+ return path.resolve(startDir);
58
+ }
59
+
60
+ // Resolve the scope for `sdoc library ls`. Explicit path arg wins;
61
+ // otherwise walk up from cwd to a git root, fall back to cwd. Returns
62
+ // the absolute path that will be both the preamble label and the
63
+ // prefix filter against indexed entries.
64
+ function resolveLsScope(explicitArg) {
65
+ if (explicitArg) {
66
+ const abs = path.resolve(explicitArg);
67
+ if (!fs.existsSync(abs)) {
68
+ console.error(`sdoc library ls: path not found: ${explicitArg}`);
69
+ process.exit(1);
70
+ }
71
+ return abs;
72
+ }
73
+ return resolveProjectRoot(process.cwd());
74
+ }
75
+
76
+ // Return entries whose user-visible path (rescuedFrom when ephemeral,
77
+ // otherwise path) is at or under the scope. Compares both the literal
78
+ // path and its realpath against both the literal scope and the realpath
79
+ // of the scope, so a symlinked /var on macOS doesn't make an entry
80
+ // disappear from `sdoc library ls`.
81
+ function entriesUnderScope(scope) {
82
+ const root = path.resolve(scope);
83
+ let rootReal = root;
84
+ try { rootReal = fs.realpathSync(root); } catch (_) {}
85
+ const sep = path.sep;
86
+ function under(p) {
87
+ if (!p) return false;
88
+ if (p === root || p.startsWith(root + sep)) return true;
89
+ if (rootReal !== root && (p === rootReal || p.startsWith(rootReal + sep))) return true;
90
+ return false;
91
+ }
92
+ const out = [];
93
+ for (const e of store.loadIndex().entries) {
94
+ const p = e.rescued && e.rescuedFrom ? e.rescuedFrom : e.path;
95
+ let pReal = p;
96
+ try { pReal = fs.realpathSync(p); } catch (_) {}
97
+ if (under(p) || under(pReal)) {
98
+ out.push(Object.assign({}, e, { userPath: p }));
99
+ }
100
+ }
101
+ out.sort((a, b) => a.userPath.localeCompare(b.userPath));
102
+ return out;
103
+ }
104
+
105
+ // `sdoc library ls [path]` and `sdoc library ls [path] --tags`.
106
+ // Designed for agents: every output starts with a preamble line that
107
+ // states the resolved scope, and ends with a count line. Same shape
108
+ // whether the result set is large, small, or empty - so an agent can
109
+ // always tell what it queried without guessing.
110
+ function libraryLs(opts) {
111
+ const scope = resolveLsScope(opts.extra);
112
+
113
+ if (opts.tagsFlag) {
114
+ const tags = libIndex.tagsUnderPrefix(scope);
115
+ if (!tags.length) {
116
+ console.log(`no tagged markdown files indexed under ${scope} yet`);
117
+ console.log(`(tip: run \`sdoc library rebuild\` if you expected results, or open a file with \`sdoc <file> +tag\` to start tagging)`);
118
+ return;
119
+ }
120
+ console.log(`most frequent tags for tagged markdown files under ${scope} (tag - count):`);
121
+ for (const { tag, count } of tags) {
122
+ console.log(` ${tag} - ${count}`);
123
+ }
124
+ // taggedFiles is the count of entries (under scope) that have at
125
+ // least one tag; tags.length is the count of distinct tags.
126
+ const entries = entriesUnderScope(scope);
127
+ const taggedFiles = entries.filter(e => (e.tags || []).length > 0).length;
128
+ console.log(`(${tags.length} distinct ${tags.length === 1 ? 'tag' : 'tags'} across ${taggedFiles} tagged ${taggedFiles === 1 ? 'file' : 'files'})`);
129
+ return;
130
+ }
131
+
132
+ const entries = entriesUnderScope(scope);
133
+ if (!entries.length) {
134
+ console.log(`library has no markdown indexed under ${scope} yet`);
135
+ console.log(`(tip: run \`sdoc library rebuild\` to scan, or open a file with \`sdoc <file>\` to index it)`);
136
+ return;
137
+ }
138
+
139
+ console.log(`library files for ${scope}:`);
140
+ // Column width: longest relative path, capped at 60 so very deep paths
141
+ // don't push the tags column off-screen. Aligned padding helps a
142
+ // human skim AND keeps the columns parseable for an agent.
143
+ const rels = entries.map(e => {
144
+ const rel = path.relative(scope, e.userPath);
145
+ return rel === '' ? path.basename(e.userPath) : rel;
146
+ });
147
+ const colWidth = Math.min(60, Math.max(...rels.map(r => r.length)));
148
+ for (let i = 0; i < entries.length; i++) {
149
+ const e = entries[i];
150
+ const rel = rels[i];
151
+ const pad = rel.length < colWidth ? ' '.repeat(colWidth - rel.length) : '';
152
+ const tagBox = (e.tags && e.tags.length)
153
+ ? '[' + e.tags.join(', ') + ']'
154
+ : '[no tags]';
155
+ console.log(` ${rel}${pad} ${tagBox}`);
156
+ }
157
+ console.log(`(${entries.length} ${entries.length === 1 ? 'file' : 'files'})`);
158
+ }
159
+
160
+ function libraryHelp() {
161
+ console.log(helpText.LIBRARY_HELP);
162
+ }
163
+
164
+ // Ping the canonical agent port; resolves true if it answers OK.
165
+ function pingAgent(port, timeoutMs = 400) {
166
+ return new Promise((resolve) => {
167
+ const req = http.get({ hostname: '127.0.0.1', port, path: '/api/library/health', timeout: timeoutMs }, (res) => {
168
+ resolve(res.statusCode === 200);
169
+ res.resume();
170
+ });
171
+ req.on('error', () => resolve(false));
172
+ req.on('timeout', () => { req.destroy(); resolve(false); });
173
+ });
174
+ }
175
+
176
+ // Default-on autostart: silently enable the OS auto-launch the first
177
+ // time the user runs `sdoc library`, unless they've explicitly disabled
178
+ // it before. The user can always turn it off with `sdoc library
179
+ // autostart disable`. Stays quiet on platforms that don't support it.
180
+ function ensureAutostart() {
181
+ if (!autostart.isSupported()) return;
182
+ const state = store.loadState();
183
+ if (state.autostartUserDisabled) return;
184
+ if (autostart.isEnabled()) return;
185
+ const r = autostart.enable();
186
+ if (r.ok) {
187
+ console.log('library: auto-start on login is on (run `sdoc library autostart disable` to turn off)');
188
+ }
189
+ }
190
+
191
+ async function libraryOpen() {
192
+ const state = store.loadState();
193
+ const siteUrl = process.env.SDOCS_URL || DEFAULT_URL;
194
+ const idx = store.loadIndex();
195
+
196
+ // If an agent is already listening on the canonical port (likely
197
+ // because autostart is enabled), don't start another - just open the
198
+ // page and exit. Avoids a port conflict and avoids the user having
199
+ // two agents.
200
+ const existing = await pingAgent(libServer.DEFAULT_PORT);
201
+ if (existing) {
202
+ const pageUrl = `${siteUrl}/library`;
203
+ console.log(`library: ${pageUrl} (using already-running agent)`);
204
+ console.log(`library: ${idx.entries.length} entries indexed`);
205
+ openBrowser(pageUrl);
206
+ return;
207
+ }
208
+
209
+ const { agentUrl } = await libServer.createServer();
210
+ const pageUrl = `${siteUrl}/library?agent=${encodeURIComponent(agentUrl)}`;
211
+ console.log(`library: ${pageUrl}`);
212
+ console.log(`library: ${idx.entries.length} entries indexed` + (state.enabled === false ? ' (scanning disabled)' : ''));
213
+ if (!idx.entries.length) console.log('library: click "rescan" in the UI to walk your home for markdown.');
214
+ ensureAutostart();
215
+ console.log(`library: agent at ${agentUrl} (ctrl-c to stop)`);
216
+ openBrowser(pageUrl);
217
+ }
218
+
219
+ function autostartEnable() {
220
+ const r = autostart.enable();
221
+ if (!r.ok) { console.error('library autostart: ' + r.message); process.exit(1); }
222
+ // Clear the "user explicitly disabled" flag so future `sdoc library`
223
+ // invocations don't tip-toe around the preference.
224
+ const s = store.loadState();
225
+ s.autostartUserDisabled = false;
226
+ store.saveState(s);
227
+ console.log('library autostart: enabled (plist at ' + r.path + ')');
228
+ }
229
+
230
+ function autostartDisable() {
231
+ const r = autostart.disable();
232
+ if (!r.ok) { console.error('library autostart: ' + r.message); process.exit(1); }
233
+ // Record the explicit-disable so the default-on logic in libraryOpen
234
+ // doesn't quietly re-enable it next time.
235
+ const s = store.loadState();
236
+ s.autostartUserDisabled = true;
237
+ store.saveState(s);
238
+ console.log(r.alreadyDisabled ? 'library autostart: was not enabled' : 'library autostart: disabled');
239
+ }
240
+
241
+ function autostartStatus() {
242
+ const s = autostart.status();
243
+ if (!s.supported) {
244
+ console.log('library autostart: not supported on ' + process.platform + ' yet (macOS only for now)');
245
+ return;
246
+ }
247
+ console.log('library autostart: ' + (s.enabled ? 'enabled' : 'disabled'));
248
+ console.log(' plist: ' + s.plistPath);
249
+ }
250
+
251
+ // The library verb dispatches on opts.file (which io.parseArgs put the
252
+ // sub-sub-verb into - it's the next positional after 'library').
253
+ // `sdoc library autostart enable` carries the second sub-arg in opts.extra.
254
+ async function libraryCommand(opts) {
255
+ // `sdoc library --help` and `sdoc library help` both print the
256
+ // library-specific long help (LIBRARY_HELP).
257
+ if (opts.helpFlag) { libraryHelp(); return; }
258
+ const sub = (opts.file || '').toLowerCase();
259
+ switch (sub) {
260
+ case '': await libraryOpen(); break;
261
+ case 'help': libraryHelp(); break;
262
+ case 'ls': libraryLs(opts); break;
263
+ case 'enable': libraryEnable(); break;
264
+ case 'disable': libraryDisable(); break;
265
+ case 'status': libraryStatus(); break;
266
+ case 'rebuild': libraryRebuild(); break;
267
+ case 'autostart': {
268
+ const action = (opts.extra || '').toLowerCase();
269
+ if (action === 'enable') autostartEnable();
270
+ else if (action === 'disable') autostartDisable();
271
+ else if (action === '' || action === 'status') autostartStatus();
272
+ else {
273
+ console.error(`sdoc library autostart: unknown action "${action}"`);
274
+ console.error('usage: sdoc library autostart [enable|disable|status]');
275
+ process.exit(1);
276
+ }
277
+ break;
278
+ }
279
+ default:
280
+ console.error(`sdoc library: unknown subcommand "${sub}"`);
281
+ console.error('usage: sdoc library [ls|enable|disable|status|rebuild|autostart|help]');
282
+ process.exit(1);
283
+ }
284
+ }
285
+
286
+ // Hook called from the default open command. Fires after the file has
287
+ // been resolved but before (or alongside) the browser open. Best-effort:
288
+ // any failure is swallowed so a bad library doesn't break opening a file.
289
+ function tapOpen(opts) {
290
+ try {
291
+ const s = store.loadState();
292
+ if (s.enabled === false) return;
293
+ if (!opts.file) return;
294
+ const abs = path.resolve(opts.file);
295
+ libIndex.indexFile(abs, { addTags: opts.addTags || [] });
296
+ } catch (_) {
297
+ // intentional: never break the open flow
298
+ }
299
+ }
300
+
301
+ module.exports = {
302
+ libraryCommand,
303
+ libraryEnable, libraryDisable, libraryStatus, libraryRebuild, libraryOpen,
304
+ libraryLs, libraryHelp,
305
+ resolveProjectRoot, resolveLsScope, entriesUnderScope,
306
+ tapOpen,
307
+ };