sdocs-dev 1.6.1 → 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/bin/sdocs-bridge.js +974 -0
- package/bin/sdocs-dev.js +145 -2102
- package/bin/sdocs-icon-names.js +1965 -0
- package/lib/agent-block.js +245 -0
- package/lib/agent-files.js +162 -0
- package/lib/bridge-commands.js +171 -0
- package/lib/cells-transclude.js +111 -0
- package/lib/commands.js +291 -0
- package/lib/constants.js +283 -0
- package/lib/help-text.js +2706 -0
- package/lib/io.js +173 -0
- package/lib/library-autostart.js +145 -0
- package/lib/library-commands.js +307 -0
- package/lib/library-ephemeral.js +111 -0
- package/lib/library-index.js +280 -0
- package/lib/library-paths.js +20 -0
- package/lib/library-scan.js +258 -0
- package/lib/library-server.js +400 -0
- package/lib/library-store.js +141 -0
- package/lib/router.js +52 -0
- package/lib/safe.js +200 -0
- package/lib/setup.js +332 -0
- package/lib/short-link.js +105 -0
- package/lib/styles.js +91 -0
- package/lib/update-check.js +163 -0
- package/lib/url.js +111 -0
- package/package.json +5 -18
- package/shared/sdocs-contrast.js +196 -0
- package/shared/sdocs-form-block.js +605 -0
- package/shared/sdocs-library-tags.js +41 -0
- package/{public → shared}/sdocs-styles.js +134 -5
- package/README.md +0 -149
- /package/{public → shared}/sdocs-slugify.js +0 -0
- /package/{public → shared}/sdocs-yaml.js +0 -0
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
+
};
|