sdocs-dev 1.4.2 → 1.6.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.
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // One-line hint after `npm i -g sdocs-dev`. Non-interactive by design.
3
+ //
4
+ // Skipped on:
5
+ // - CI=true (automated builds)
6
+ // - NO_UPDATE_NOTIFIER=1 (user opt-out)
7
+ // - npm installing as a dependency (not a global install)
8
+ //
9
+ // NOT gated on isTTY: when an agent installs sdoc on a user's behalf, the
10
+ // agent reads stdout, so the hint reaches the user via the agent's summary.
11
+
12
+ if (process.env.CI) process.exit(0);
13
+ if (process.env.NO_UPDATE_NOTIFIER) process.exit(0);
14
+
15
+ // `npm_config_global` is set when this is a global install (`npm i -g`).
16
+ // For local installs (as a dependency), stay silent.
17
+ if (process.env.npm_config_global !== 'true') process.exit(0);
18
+
19
+ console.log(`
20
+ sdocs-dev installed. Run \`sdoc\` to wire SDocs into your CLI coding agents.
21
+
22
+ This allows you to use SDocs in conversation with a CLI coding agent.
23
+ Try asking:
24
+ "write up the plan and sdoc it to me"
25
+ "explain async/await to me in a sdoc"
26
+ "draft the release notes as a sdoc I can share"
27
+ `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdocs-dev",
3
- "version": "1.4.2",
3
+ "version": "1.6.1",
4
4
  "description": "Open, share, and style markdown files from the terminal",
5
5
  "main": "bin/sdocs-dev.js",
6
6
  "bin": {
@@ -15,7 +15,8 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "start": "node server.js",
18
- "test": "node test/run.js"
18
+ "test": "node test/run.js",
19
+ "postinstall": "node bin/sdocs-postinstall.js"
19
20
  },
20
21
  "keywords": [
21
22
  "markdown",
@@ -31,12 +32,14 @@
31
32
  "license": "MIT",
32
33
  "repository": {
33
34
  "type": "git",
34
- "url": "git+https://github.com/JoshInLisbon/SDocs.git"
35
+ "url": "git+https://github.com/espressoplease/SDocs.git"
35
36
  },
36
37
  "homepage": "https://sdocs.dev",
38
+ "dependencies": {
39
+ "better-sqlite3": "^12.8.0"
40
+ },
37
41
  "devDependencies": {
38
42
  "@playwright/test": "^1.59.1",
39
- "better-sqlite3": "^12.8.0",
40
43
  "brotli": "^1.3.3",
41
44
  "brotli-dec-wasm": "^2.3.2",
42
45
  "brotli-wasm": "^3.0.1",
@@ -1,10 +1,30 @@
1
1
  // sdocs-yaml.js — YAML front matter parse/serialize (UMD)
2
- // Shared by browser (app) and Node (CLI + tests)
2
+ // Shared by browser (app) and Node (CLI + tests).
3
+ // Supports a subset of YAML: nested maps, inline `{...}` leaf maps, and
4
+ // arrays of scalars or objects serialized as `- item` blocks.
3
5
  (function (exports) {
4
6
  'use strict';
5
7
 
8
+ // Skip keys that would replace the object's prototype chain or shadow
9
+ // built-ins via the cascade. A shared YAML doc cannot smuggle ghost
10
+ // properties into `meta.comments` etc. by setting `__proto__:` to a
11
+ // nested map.
12
+ function isUnsafeKey(k) {
13
+ return k === '__proto__' || k === 'prototype' || k === 'constructor';
14
+ }
15
+
6
16
  function parseScalar(v) {
7
- v = v.trim().replace(/^["']|["']$/g, '');
17
+ v = v.trim();
18
+ if (v === '') return v;
19
+ // Quoted string: strip quotes and handle minimal escapes.
20
+ if ((v[0] === '"' && v[v.length - 1] === '"') ||
21
+ (v[0] === "'" && v[v.length - 1] === "'")) {
22
+ var quoted = v.slice(1, -1);
23
+ if (v[0] === '"') {
24
+ quoted = quoted.replace(/\\"/g, '"').replace(/\\\\/g, '\\').replace(/\\n/g, '\n');
25
+ }
26
+ return quoted;
27
+ }
8
28
  const n = Number(v);
9
29
  return (!isNaN(n) && v !== '') ? n : v;
10
30
  }
@@ -14,30 +34,73 @@ function parseInlineObject(str) {
14
34
  const obj = {};
15
35
  inner.split(',').forEach(pair => {
16
36
  const m = pair.trim().match(/^(\w[\w-]*):\s*(.*)/);
17
- if (m) obj[m[1]] = parseScalar(m[2].trim());
37
+ if (m && !isUnsafeKey(m[1])) obj[m[1]] = parseScalar(m[2].trim());
18
38
  });
19
39
  return obj;
20
40
  }
21
41
 
42
+ // Parses an array block starting at lines[startIdx] whose items are
43
+ // introduced by `- ` at exactly `indent` columns. An item is either a
44
+ // scalar (`- foo`) or an object whose first key appears on the same line
45
+ // as the dash (`- key: value`) and whose following keys are indented two
46
+ // beyond the dash.
47
+ function parseArray(lines, startIdx, indent) {
48
+ var out = [];
49
+ var i = startIdx;
50
+ var dashPrefix = ' '.repeat(indent) + '- ';
51
+ while (i < lines.length && lines[i].startsWith(dashPrefix)) {
52
+ var rest = lines[i].substring(dashPrefix.length);
53
+ var km = rest.match(/^(\w[\w-]*):\s*(.*)/);
54
+ if (km) {
55
+ // Object item. Collect this line's key + any keys indented further.
56
+ var obj = {};
57
+ var key = km[1], val = km[2].trim();
58
+ if (!isUnsafeKey(key)) obj[key] = parseScalar(val);
59
+ i++;
60
+ var itemPrefix = ' '.repeat(indent + 2);
61
+ while (i < lines.length && lines[i].startsWith(itemPrefix) && !lines[i].startsWith(dashPrefix)) {
62
+ var line = lines[i].substring(indent + 2);
63
+ var im = line.match(/^(\w[\w-]*):\s*(.*)/);
64
+ if (im && !isUnsafeKey(im[1])) obj[im[1]] = parseScalar(im[2]);
65
+ i++;
66
+ }
67
+ out.push(obj);
68
+ } else {
69
+ out.push(parseScalar(rest));
70
+ i++;
71
+ }
72
+ }
73
+ return { arr: out, nextIdx: i };
74
+ }
75
+
22
76
  function parseBlock(lines, startIdx, indent) {
23
77
  const result = {};
24
78
  let i = startIdx;
25
79
  const prefix = new RegExp('^' + ' '.repeat(indent));
26
80
  const deeper = new RegExp('^' + ' '.repeat(indent + 2));
81
+ const dashDeeper = new RegExp('^' + ' '.repeat(indent + 2) + '- ');
82
+ const dashSame = new RegExp('^' + ' '.repeat(indent) + '- ');
27
83
  while (i < lines.length && prefix.test(lines[i])) {
28
84
  const nl = lines[i].substring(indent);
29
85
  const nm = nl.match(/^(\w[\w-]*):\s*(.*)/);
30
86
  if (!nm) { i++; continue; }
31
87
  const key = nm[1], rest = nm[2].trim();
88
+ const safe = !isUnsafeKey(key);
32
89
  if (rest.startsWith('{')) {
33
- result[key] = parseInlineObject(rest); i++;
34
- } else if (rest === '' && i + 1 < lines.length && deeper.test(lines[i + 1])) {
90
+ const v = parseInlineObject(rest); if (safe) result[key] = v; i++;
91
+ } else if (rest === '' && i + 1 < lines.length && dashDeeper.test(lines[i + 1])) {
92
+ // Array child: `key:` followed by ` - ...` lines.
93
+ i++;
94
+ var arr = parseArray(lines, i, indent + 2);
95
+ if (safe) result[key] = arr.arr;
96
+ i = arr.nextIdx;
97
+ } else if (rest === '' && i + 1 < lines.length && deeper.test(lines[i + 1]) && !dashSame.test(lines[i + 1])) {
35
98
  i++;
36
99
  var sub = parseBlock(lines, i, indent + 2);
37
- result[key] = sub.obj;
100
+ if (safe) result[key] = sub.obj;
38
101
  i = sub.nextIdx;
39
102
  } else {
40
- result[key] = parseScalar(rest); i++;
103
+ if (safe) result[key] = parseScalar(rest); i++;
41
104
  }
42
105
  }
43
106
  return { obj: result, nextIdx: i };
@@ -62,13 +125,50 @@ function hasNestedObjects(obj) {
62
125
  return false;
63
126
  }
64
127
 
128
+ function serializeArrayItems(arr, indent) {
129
+ const lines = [];
130
+ const pad = ' '.repeat(indent);
131
+ for (const item of arr) {
132
+ if (item === null || typeof item !== 'object') {
133
+ lines.push(`${pad}- ${JSON.stringify(item)}`);
134
+ } else {
135
+ const entries = Object.entries(item);
136
+ if (entries.length === 0) {
137
+ lines.push(`${pad}- {}`);
138
+ continue;
139
+ }
140
+ const [firstK, firstV] = entries[0];
141
+ lines.push(`${pad}- ${firstK}: ${JSON.stringify(firstV)}`);
142
+ for (let j = 1; j < entries.length; j++) {
143
+ const [k, v] = entries[j];
144
+ lines.push(`${pad} ${k}: ${JSON.stringify(v)}`);
145
+ }
146
+ }
147
+ }
148
+ return lines;
149
+ }
150
+
65
151
  function serializeFrontMatter(meta) {
66
152
  const lines = ['---'];
67
153
  for (const [k, v] of Object.entries(meta)) {
68
- if (typeof v === 'object' && v !== null) {
154
+ if (Array.isArray(v)) {
155
+ // For the comments list, prepend a one-line schema doc so a reader
156
+ // (human or agent) opening this file cold can interpret block ids
157
+ // and the resolved flag without grepping the source.
158
+ if (k === 'comments' && v.length) {
159
+ lines.push('# Comments: block "tag:n" = nth (0-indexed) <tag> in render order.');
160
+ lines.push('# block kind may carry block_text (first ~60 chars) as a survival hint when the index drifts.');
161
+ lines.push('# inline kind anchors via quote (+ optional prefix/suffix). resolved: true marks addressed.');
162
+ }
163
+ lines.push(`${k}:`);
164
+ for (const line of serializeArrayItems(v, 2)) lines.push(line);
165
+ } else if (typeof v === 'object' && v !== null) {
69
166
  lines.push(`${k}:`);
70
167
  for (const [sk, sv] of Object.entries(v)) {
71
- if (typeof sv === 'object' && sv !== null) {
168
+ if (Array.isArray(sv)) {
169
+ lines.push(` ${sk}:`);
170
+ for (const line of serializeArrayItems(sv, 4)) lines.push(line);
171
+ } else if (typeof sv === 'object' && sv !== null) {
72
172
  // If sub-object contains nested objects (3 levels), serialize as block
73
173
  if (hasNestedObjects(sv)) {
74
174
  lines.push(` ${sk}:`);