sdocs-dev 1.15.0 → 1.18.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-dev.js +35 -11
- package/lib/agent-block.js +253 -50
- package/lib/agent-files.js +308 -74
- package/lib/cells-verify.js +1 -0
- package/lib/cloud-bindings.js +85 -0
- package/lib/cloud-commands.js +741 -0
- package/lib/cloud-credentials.js +380 -0
- package/lib/commands.js +44 -4
- package/lib/help-text.js +272 -70
- package/lib/io.js +44 -3
- package/lib/library-commands.js +6 -15
- package/lib/library-scan.js +16 -6
- package/lib/library-server.js +4 -12
- package/lib/setup.js +145 -189
- package/lib/slides-verify.js +109 -0
- package/package.json +1 -1
- package/shared/sdocs-cells-formula.js +547 -73
- package/shared/sdocs-cells.js +164 -18
- package/shared/sdocs-shapes.js +1044 -0
- package/shared/sdocs-slide-resolve.js +258 -0
- package/shared/sdocs-slide-stdlib.js +180 -0
- package/shared/sdocs-styles.js +1 -1
package/lib/library-commands.js
CHANGED
|
@@ -30,16 +30,8 @@ function libraryDisable() {
|
|
|
30
30
|
function libraryStatus() {
|
|
31
31
|
const s = store.loadState();
|
|
32
32
|
const idx = store.loadIndex();
|
|
33
|
-
const last = s.lastScanAt ? new Date(s.lastScanAt).toISOString() : 'never';
|
|
34
33
|
console.log(`library: ${s.enabled === false ? 'disabled' : 'enabled'}`);
|
|
35
34
|
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
35
|
}
|
|
44
36
|
|
|
45
37
|
// Walk up from a directory looking for `.git/`. Falls back to the start
|
|
@@ -114,7 +106,7 @@ function libraryLs(opts) {
|
|
|
114
106
|
const tags = libIndex.tagsUnderPrefix(scope);
|
|
115
107
|
if (!tags.length) {
|
|
116
108
|
console.log(`no tagged markdown files indexed under ${scope} yet`);
|
|
117
|
-
console.log(`(tip:
|
|
109
|
+
console.log(`(tip: open a file with \`sdoc <file> +tag\` to add and tag it)`);
|
|
118
110
|
return;
|
|
119
111
|
}
|
|
120
112
|
console.log(`most frequent tags for tagged markdown files under ${scope} (tag - count):`);
|
|
@@ -132,7 +124,7 @@ function libraryLs(opts) {
|
|
|
132
124
|
const entries = entriesUnderScope(scope);
|
|
133
125
|
if (!entries.length) {
|
|
134
126
|
console.log(`library has no markdown indexed under ${scope} yet`);
|
|
135
|
-
console.log(`(tip:
|
|
127
|
+
console.log(`(tip: open a file with \`sdoc <file>\` to add it)`);
|
|
136
128
|
return;
|
|
137
129
|
}
|
|
138
130
|
|
|
@@ -209,8 +201,8 @@ async function libraryOpen() {
|
|
|
209
201
|
const { agentUrl } = await libServer.createServer();
|
|
210
202
|
const pageUrl = `${siteUrl}/library?agent=${encodeURIComponent(agentUrl)}`;
|
|
211
203
|
console.log(`library: ${pageUrl}`);
|
|
212
|
-
console.log(`library: ${idx.entries.length} entries indexed` + (state.enabled === false ? ' (
|
|
213
|
-
if (!idx.entries.length) console.log('library:
|
|
204
|
+
console.log(`library: ${idx.entries.length} entries indexed` + (state.enabled === false ? ' (indexing disabled)' : ''));
|
|
205
|
+
if (!idx.entries.length) console.log('library: open a file with `sdoc <file>` to add it.');
|
|
214
206
|
ensureAutostart();
|
|
215
207
|
console.log(`library: agent at ${agentUrl} (ctrl-c to stop)`);
|
|
216
208
|
openBrowser(pageUrl);
|
|
@@ -263,7 +255,6 @@ async function libraryCommand(opts) {
|
|
|
263
255
|
case 'enable': libraryEnable(); break;
|
|
264
256
|
case 'disable': libraryDisable(); break;
|
|
265
257
|
case 'status': libraryStatus(); break;
|
|
266
|
-
case 'rebuild': libraryRebuild(); break;
|
|
267
258
|
case 'autostart': {
|
|
268
259
|
const action = (opts.extra || '').toLowerCase();
|
|
269
260
|
if (action === 'enable') autostartEnable();
|
|
@@ -278,7 +269,7 @@ async function libraryCommand(opts) {
|
|
|
278
269
|
}
|
|
279
270
|
default:
|
|
280
271
|
console.error(`sdoc library: unknown subcommand "${sub}"`);
|
|
281
|
-
console.error('usage: sdoc library [ls|enable|disable|status|
|
|
272
|
+
console.error('usage: sdoc library [ls|enable|disable|status|autostart|help]');
|
|
282
273
|
process.exit(1);
|
|
283
274
|
}
|
|
284
275
|
}
|
|
@@ -300,7 +291,7 @@ function tapOpen(opts) {
|
|
|
300
291
|
|
|
301
292
|
module.exports = {
|
|
302
293
|
libraryCommand,
|
|
303
|
-
libraryEnable, libraryDisable, libraryStatus,
|
|
294
|
+
libraryEnable, libraryDisable, libraryStatus, libraryOpen,
|
|
304
295
|
libraryLs, libraryHelp,
|
|
305
296
|
resolveProjectRoot, resolveLsScope, entriesUnderScope,
|
|
306
297
|
tapOpen,
|
package/lib/library-scan.js
CHANGED
|
@@ -15,18 +15,13 @@ const paths = require('./library-paths');
|
|
|
15
15
|
const DEFAULT_MAX_SIZE = 1 * 1024 * 1024;
|
|
16
16
|
|
|
17
17
|
const DIRNAME_BLOCKLIST = new Set([
|
|
18
|
-
'node_modules',
|
|
19
18
|
'.git',
|
|
20
19
|
'.svn',
|
|
21
20
|
'.hg',
|
|
22
|
-
'dist',
|
|
23
|
-
'build',
|
|
24
|
-
'vendor',
|
|
25
21
|
'.venv',
|
|
26
22
|
'.next',
|
|
27
23
|
'.cache',
|
|
28
24
|
'__pycache__',
|
|
29
|
-
'target',
|
|
30
25
|
'.gradle',
|
|
31
26
|
'.idea',
|
|
32
27
|
'.vscode',
|
|
@@ -46,6 +41,19 @@ const DIRNAME_BLOCKLIST = new Set([
|
|
|
46
41
|
'.password-store',
|
|
47
42
|
]);
|
|
48
43
|
|
|
44
|
+
// Directories skipped during scanning traversal for speed, but NOT blocked
|
|
45
|
+
// from path-gating when opening an explicitly indexed file.
|
|
46
|
+
const SCAN_SKIP_DIRNAMES = new Set([
|
|
47
|
+
'assets',
|
|
48
|
+
'graphify',
|
|
49
|
+
'node_modules',
|
|
50
|
+
'vendor',
|
|
51
|
+
'target',
|
|
52
|
+
'dist',
|
|
53
|
+
'build',
|
|
54
|
+
'venv',
|
|
55
|
+
]);
|
|
56
|
+
|
|
49
57
|
// File basenames that should never make it into the library, regardless
|
|
50
58
|
// of which directory they live in or what extension they carry. Most of
|
|
51
59
|
// these don't have a markdown extension and so the existing extension
|
|
@@ -129,8 +137,10 @@ function deniedByPattern(absPath) {
|
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
function shouldSkipDir(absDir, base, skipSet, exemptRoots) {
|
|
132
|
-
|
|
140
|
+
// Exempt .sdocs so AI coding agent markdown artifacts are indexed
|
|
141
|
+
if (base.startsWith('.') && base !== '.' && base !== '..' && base !== '.sdocs') return true;
|
|
133
142
|
if (DIRNAME_BLOCKLIST.has(base)) return true;
|
|
143
|
+
if (SCAN_SKIP_DIRNAMES.has(base)) return true;
|
|
134
144
|
if (skipSet.has(absDir)) return true;
|
|
135
145
|
// Skip ephemeral paths during descent unless we're inside a root that
|
|
136
146
|
// the caller explicitly named (in which case they want it scanned).
|
package/lib/library-server.js
CHANGED
|
@@ -35,9 +35,8 @@ try {
|
|
|
35
35
|
// 2. The deny-pattern list (SSH keys, .env, credentials.{json,...}
|
|
36
36
|
// and anything under .ssh/.aws/.gnupg/...).
|
|
37
37
|
// 3. Library-membership: the real path must appear in the index.
|
|
38
|
-
// The index
|
|
39
|
-
//
|
|
40
|
-
// are refused.
|
|
38
|
+
// The index contains files the user explicitly opened with sdoc;
|
|
39
|
+
// arbitrary paths outside that set are refused.
|
|
41
40
|
//
|
|
42
41
|
// Returns { ok: true, realPath } on pass, { ok: false, reason, status }
|
|
43
42
|
// on refusal. Caller picks the HTTP status from `status`.
|
|
@@ -295,12 +294,6 @@ function createServer({ port } = {}) {
|
|
|
295
294
|
return;
|
|
296
295
|
}
|
|
297
296
|
|
|
298
|
-
if (req.method === 'POST' && route === '/api/library/rescan') {
|
|
299
|
-
const result = libIndex.scanAndIndex();
|
|
300
|
-
sendJson(res, 200, result);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
297
|
// Serve the current contents of a local file. The editor page
|
|
305
298
|
// uses this to refresh content after the URL-hash snapshot goes
|
|
306
299
|
// stale (e.g. after the user edited tags then reloaded). Gated
|
|
@@ -319,9 +312,8 @@ function createServer({ port } = {}) {
|
|
|
319
312
|
}
|
|
320
313
|
|
|
321
314
|
// Re-index a single file. Called by the editor page after a Bridge
|
|
322
|
-
// save so the library catches up immediately
|
|
323
|
-
//
|
|
324
|
-
// the file the path points at.
|
|
315
|
+
// save so the library catches up immediately. Pure read-then-index;
|
|
316
|
+
// never writes the file the path points at.
|
|
325
317
|
if (req.method === 'POST' && route === '/api/library/reindex') {
|
|
326
318
|
const body = await readBody(req);
|
|
327
319
|
const filePath = body && body.path;
|
package/lib/setup.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
// `sdoc setup`, `sdoc refresh`, `sdoc auto-update`, and the implicit
|
|
2
|
-
// post-command refresh that keeps
|
|
2
|
+
// post-command refresh that keeps the SmallDocs skill current as new sdoc
|
|
3
3
|
// versions ship.
|
|
4
4
|
//
|
|
5
|
-
// runSetup: first-run interactive flow. Detects
|
|
6
|
-
// the
|
|
7
|
-
//
|
|
8
|
-
// runRefresh: unconditional refresh of
|
|
9
|
-
// has a recognised block.
|
|
5
|
+
// runSetup: first-run interactive flow. Detects installed agents, installs
|
|
6
|
+
// the skill (canonical copy + symlinks), strips legacy blocks. Pass
|
|
7
|
+
// dryRun:true to preview without touching anything.
|
|
8
|
+
// runRefresh: unconditional refresh of the canonical skill + symlinks.
|
|
10
9
|
// runAutoUpdateSubcommand: flips state.autoInstallUpdates.
|
|
11
10
|
// maybeAutoRefresh: called after every successful command. Quiet, only
|
|
12
|
-
//
|
|
11
|
+
// rewrites the canonical skill when its version is stale.
|
|
13
12
|
|
|
14
|
-
const os
|
|
13
|
+
const os = require('os');
|
|
15
14
|
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
16
|
const readline = require('readline');
|
|
17
17
|
|
|
18
18
|
const {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
SKILL_VERSION,
|
|
20
|
+
SKILL_BODY,
|
|
21
|
+
SKILL_NAME,
|
|
22
|
+
formatSkill,
|
|
23
|
+
canonicalSkillFile,
|
|
24
|
+
canonicalSkillDir,
|
|
25
|
+
legacyBlockTargets,
|
|
26
|
+
findBookendedBlock,
|
|
27
|
+
findLegacyBlock,
|
|
22
28
|
compareVersions,
|
|
23
29
|
readSetupState,
|
|
24
30
|
writeSetupState,
|
|
@@ -28,11 +34,12 @@ const {
|
|
|
28
34
|
const { upgradeCommand } = require('./update-check');
|
|
29
35
|
|
|
30
36
|
const {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
detectSkillAgents,
|
|
38
|
+
hasSetupEvidence,
|
|
39
|
+
syncAgentSkill,
|
|
40
|
+
syncChanged,
|
|
41
|
+
toImplicitResults,
|
|
42
|
+
printSyncSummary,
|
|
36
43
|
} = require('./agent-files');
|
|
37
44
|
|
|
38
45
|
const { VERSION, AGENT_CHANGES_URL } = require('./constants');
|
|
@@ -59,18 +66,56 @@ async function askAutoInstallConsent() {
|
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
async function askAutoRefreshConsent() {
|
|
62
|
-
console.log('\nKeep this
|
|
69
|
+
console.log('\nKeep this skill updated on future sdoc upgrades?');
|
|
63
70
|
console.log('');
|
|
64
|
-
console.log('When sdoc adds a feature we sometimes update
|
|
65
|
-
console.log('
|
|
66
|
-
console.log(
|
|
67
|
-
console.log('showing the exact delta - the new wording, and why it changed.');
|
|
71
|
+
console.log('When sdoc adds a feature we sometimes update the skill so your');
|
|
72
|
+
console.log('agent learns about it. Each change prints a notice with a link to');
|
|
73
|
+
console.log(`${AGENT_CHANGES_URL} showing the exact delta - the new wording, and why.`);
|
|
68
74
|
console.log('');
|
|
69
75
|
console.log('Re-run `sdoc setup` any time to change this.\n');
|
|
70
76
|
const a = await ask('Enable? [Y/n] ');
|
|
71
77
|
return !a || a === 'y' || a === 'yes';
|
|
72
78
|
}
|
|
73
79
|
|
|
80
|
+
// Preview what setup would do: print the skill, the symlinks it would create,
|
|
81
|
+
// the agents covered by the canonical copy, and any legacy blocks it would
|
|
82
|
+
// strip. Touches no file and writes no state.
|
|
83
|
+
function dryRunPreview() {
|
|
84
|
+
const home = os.homedir();
|
|
85
|
+
const env = process.env;
|
|
86
|
+
const skillPath = canonicalSkillFile(home);
|
|
87
|
+
console.log(`--- ${skillPath} ---`);
|
|
88
|
+
console.log(formatSkill(SKILL_VERSION));
|
|
89
|
+
|
|
90
|
+
const detected = detectSkillAgents(home, env);
|
|
91
|
+
const linked = detected.filter(a => !a.universal);
|
|
92
|
+
const universal = detected.filter(a => a.universal);
|
|
93
|
+
|
|
94
|
+
if (universal.length) {
|
|
95
|
+
console.log('\nCovered by the canonical copy (~/.agents/skills, no symlink needed):');
|
|
96
|
+
for (const a of universal) console.log(` ${a.displayName}`);
|
|
97
|
+
}
|
|
98
|
+
if (linked.length) {
|
|
99
|
+
console.log('\nSymlinks to create (<agent skills dir> -> canonical):');
|
|
100
|
+
for (const a of linked) console.log(` ${path.join(a.dir, SKILL_NAME)} -> ${canonicalSkillDir(home)}`);
|
|
101
|
+
}
|
|
102
|
+
if (detected.length === 0) {
|
|
103
|
+
console.log('\nNo coding-agent configs detected. The canonical skill is still written');
|
|
104
|
+
console.log('so any agent that discovers ~/.agents/skills picks it up.');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const wouldStrip = [];
|
|
108
|
+
for (const t of legacyBlockTargets(home, env)) {
|
|
109
|
+
let content;
|
|
110
|
+
try { content = fs.readFileSync(t.file, 'utf-8'); } catch (_) { continue; }
|
|
111
|
+
if (findBookendedBlock(content) || findLegacyBlock(content)) wouldStrip.push(t.file);
|
|
112
|
+
}
|
|
113
|
+
if (wouldStrip.length) {
|
|
114
|
+
console.log('\nLegacy SmallDocs blocks to remove:');
|
|
115
|
+
for (const f of wouldStrip) console.log(` ${f}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
74
119
|
async function runSetup({ force = false, yes = false, dryRun = false } = {}) {
|
|
75
120
|
if (!force) {
|
|
76
121
|
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
@@ -79,143 +124,67 @@ async function runSetup({ force = false, yes = false, dryRun = false } = {}) {
|
|
|
79
124
|
}
|
|
80
125
|
|
|
81
126
|
// ── --yes (non-interactive) path ───────────────────────────
|
|
82
|
-
// Pulled out of the detection branch so this is the SINGLE place that
|
|
83
|
-
// handles every --yes case: fresh install, old block (upgrade), legacy
|
|
84
|
-
// open-marker (migration), already-current (no-op), no agents at all.
|
|
85
|
-
// Idempotent by design - an agent or user can re-paste the install prompt
|
|
86
|
-
// any number of times and the result is a current block in every detected
|
|
87
|
-
// config, or a clean "nothing to do".
|
|
88
127
|
if (yes) {
|
|
89
|
-
|
|
90
|
-
// Prints each file path and the block that would be written, then exits
|
|
91
|
-
// without touching any file or mutating setup state. Must return before
|
|
92
|
-
// the write steps below.
|
|
93
|
-
if (dryRun) {
|
|
94
|
-
const toWrite = detectAgents().filter(t => !fileHasBlock(t.filePath));
|
|
95
|
-
if (toWrite.length === 0) {
|
|
96
|
-
console.log('All SDocs agent blocks already at current version. Nothing to do.');
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
for (const t of toWrite) {
|
|
100
|
-
console.log(`--- ${t.filePath} ---`);
|
|
101
|
-
console.log(formatAgentBlock(AGENT_BLOCK_VERSION, AGENT_BLOCK_BODY));
|
|
102
|
-
}
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
128
|
+
if (dryRun) { dryRunPreview(); return; }
|
|
105
129
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Step 2: any agent whose config dir exists but doesn't yet have a
|
|
114
|
-
// block gets one written. Re-detect after refresh because the refresh
|
|
115
|
-
// step may have flipped some files from "needs block" to "has block".
|
|
116
|
-
const stillMissing = detectAgents().filter(t => !fileHasBlock(t.filePath));
|
|
117
|
-
const writtenTo = [];
|
|
118
|
-
for (const t of stillMissing) {
|
|
119
|
-
try { writeBookendedBlock(t.filePath); writtenTo.push(t.filePath); console.log(`✓ ${t.name}: ${t.filePath}`); }
|
|
120
|
-
catch (e) { console.error(`✗ ${t.name}: ${e.message}`); }
|
|
130
|
+
const result = syncAgentSkill({});
|
|
131
|
+
const changed = syncChanged(result);
|
|
132
|
+
const detected = detectSkillAgents(os.homedir(), process.env);
|
|
133
|
+
|
|
134
|
+
if (changed || result.errors.length) {
|
|
135
|
+
printSyncSummary(result);
|
|
121
136
|
}
|
|
137
|
+
if (result.errors.length) return;
|
|
122
138
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const anyAgentDir = detectAgents().length > 0;
|
|
127
|
-
writeSetupState({
|
|
128
|
-
setupCompleted: new Date().toISOString(),
|
|
129
|
-
writtenTo: [], declined: !anyAgentDir,
|
|
130
|
-
autoRefreshAgentFiles: anyAgentDir,
|
|
131
|
-
autoInstallUpdates: false,
|
|
132
|
-
lastRunVersion: VERSION,
|
|
133
|
-
});
|
|
134
|
-
if (anyAgentDir) {
|
|
135
|
-
console.log('All SDocs agent blocks already at current version. Nothing to do.');
|
|
139
|
+
if (!changed) {
|
|
140
|
+
if (detected.length > 0) {
|
|
141
|
+
console.log('SmallDocs skill already at current version. Nothing to do.');
|
|
136
142
|
} else {
|
|
137
|
-
console.log('No coding-agent configs detected.
|
|
138
|
-
console.log('
|
|
143
|
+
console.log('No coding-agent configs detected. The canonical skill is at');
|
|
144
|
+
console.log('~/.agents/skills/smalldocs/SKILL.md; any agent that discovers');
|
|
145
|
+
console.log('~/.agents/skills will pick it up.');
|
|
139
146
|
}
|
|
140
|
-
return;
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
writeSetupState({
|
|
144
150
|
setupCompleted: new Date().toISOString(),
|
|
145
|
-
writtenTo:
|
|
151
|
+
writtenTo: changed ? [canonicalSkillFile(os.homedir())] : [],
|
|
152
|
+
declined: false,
|
|
146
153
|
autoRefreshAgentFiles: true,
|
|
147
154
|
autoInstallUpdates: false,
|
|
148
155
|
lastRunVersion: VERSION,
|
|
149
156
|
});
|
|
150
|
-
const n = affected.length;
|
|
151
|
-
const verb = writtenTo.length && refreshedFiles.length
|
|
152
|
-
? 'Wrote/refreshed'
|
|
153
|
-
: (writtenTo.length ? 'Wrote' : 'Refreshed');
|
|
154
|
-
console.log(`\nDone. ${verb} SDocs block in ${n} ${n === 1 ? 'file' : 'files'}.`);
|
|
155
157
|
return;
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.log('
|
|
173
|
-
console.log('
|
|
174
|
-
console.log('No coding-agent configs detected.');
|
|
175
|
-
const a = await ask('Do you use opencode? [y/N] ');
|
|
176
|
-
const writtenTo = [];
|
|
177
|
-
let autoRefresh = false;
|
|
178
|
-
let autoInstall = false;
|
|
179
|
-
if (a === 'y' || a === 'yes') {
|
|
180
|
-
const target = path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md');
|
|
181
|
-
try { writeBookendedBlock(target); writtenTo.push(target); console.log(`✓ Wrote SDocs section to ${target}`); }
|
|
182
|
-
catch (e) { console.error(`Failed to write ${target}: ${e.message}`); }
|
|
183
|
-
autoRefresh = await askAutoRefreshConsent();
|
|
184
|
-
autoInstall = await askAutoInstallConsent();
|
|
185
|
-
console.log('Done. Run `sdoc setup` any time to revisit.');
|
|
186
|
-
} else {
|
|
187
|
-
console.log('Skipped. Run `sdoc setup` any time to revisit.');
|
|
188
|
-
}
|
|
189
|
-
writeSetupState({
|
|
190
|
-
setupCompleted: new Date().toISOString(),
|
|
191
|
-
writtenTo, declined: writtenTo.length === 0,
|
|
192
|
-
autoRefreshAgentFiles: autoRefresh,
|
|
193
|
-
autoInstallUpdates: autoInstall,
|
|
194
|
-
lastRunVersion: VERSION,
|
|
195
|
-
});
|
|
196
|
-
return;
|
|
160
|
+
// ── interactive path ───────────────────────────────────────
|
|
161
|
+
const home = os.homedir();
|
|
162
|
+
const detected = detectSkillAgents(home, process.env);
|
|
163
|
+
|
|
164
|
+
console.log('\n\u2728─────── SmallDocs setup ───────\u2728');
|
|
165
|
+
console.log('Install the SmallDocs skill so your coding agents know `sdoc`.\n');
|
|
166
|
+
|
|
167
|
+
if (detected.length > 0) {
|
|
168
|
+
console.log('Detected: ' + detected.map(a => a.displayName).join(', '));
|
|
169
|
+
console.log('\nWill write the skill to ~/.agents/skills/smalldocs/SKILL.md.');
|
|
170
|
+
console.log('Agents using that universal location read it directly; other');
|
|
171
|
+
console.log('detected agents receive a symlink in their skills directory.');
|
|
172
|
+
} else {
|
|
173
|
+
console.log('No coding-agent configs detected. Setup still writes the canonical');
|
|
174
|
+
console.log('skill at ~/.agents/skills/smalldocs/SKILL.md, which any agent that');
|
|
175
|
+
console.log('discovers ~/.agents/skills will pick up.');
|
|
197
176
|
}
|
|
198
|
-
|
|
199
|
-
console.log('\n✨─────── SDocs setup ───────✨');
|
|
200
|
-
console.log('First run only - wire SDocs into your CLI coding agents.\n');
|
|
201
|
-
console.log('Detected: ' + detected.map(t => t.name).join(', '));
|
|
202
|
-
console.log('\nWill append a short SDocs section to:');
|
|
203
|
-
for (const t of detected) console.log(' ' + t.filePath);
|
|
204
|
-
console.log('\nThese files are loaded into every conversation across all your');
|
|
205
|
-
console.log('projects, so SDocs becomes available no matter where you\'re working.');
|
|
206
|
-
console.log('');
|
|
207
|
-
console.log('You can ask your agent things like:');
|
|
177
|
+
console.log('\nYou can ask your agent things like:');
|
|
208
178
|
console.log(' "write up the plan and sdoc it to me"');
|
|
209
179
|
console.log(' "explain async/await to me in a sdoc"');
|
|
210
180
|
console.log(' "draft the release notes as a sdoc I can share"');
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
console.log(
|
|
215
|
-
console.log(AGENT_BLOCK_BODY.trim());
|
|
181
|
+
|
|
182
|
+
const RULE = '\u2550'.repeat(36);
|
|
183
|
+
console.log(`\n${RULE} Skill body ${RULE}`);
|
|
184
|
+
console.log(SKILL_BODY.trim());
|
|
216
185
|
console.log(RULE);
|
|
217
186
|
|
|
218
|
-
const a = await ask('\
|
|
187
|
+
const a = await ask('\nInstall? [Y/n/skip] ');
|
|
219
188
|
const skipped = a === 'skip' || (a && a !== 'y' && a !== 'yes');
|
|
220
189
|
if (skipped) {
|
|
221
190
|
writeSetupState({
|
|
@@ -228,18 +197,18 @@ async function runSetup({ force = false, yes = false, dryRun = false } = {}) {
|
|
|
228
197
|
return;
|
|
229
198
|
}
|
|
230
199
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
200
|
+
const result = syncAgentSkill({});
|
|
201
|
+
const changed = syncChanged(result);
|
|
202
|
+
if (changed || result.errors.length) printSyncSummary(result);
|
|
203
|
+
if (result.errors.length) return;
|
|
236
204
|
|
|
237
|
-
const autoRefresh =
|
|
238
|
-
const autoInstall =
|
|
205
|
+
const autoRefresh = await askAutoRefreshConsent();
|
|
206
|
+
const autoInstall = await askAutoInstallConsent();
|
|
239
207
|
|
|
240
208
|
writeSetupState({
|
|
241
209
|
setupCompleted: new Date().toISOString(),
|
|
242
|
-
writtenTo
|
|
210
|
+
writtenTo: changed ? [canonicalSkillFile(home)] : [],
|
|
211
|
+
declined: false,
|
|
243
212
|
autoRefreshAgentFiles: autoRefresh,
|
|
244
213
|
autoInstallUpdates: autoInstall,
|
|
245
214
|
lastRunVersion: VERSION,
|
|
@@ -247,25 +216,27 @@ async function runSetup({ force = false, yes = false, dryRun = false } = {}) {
|
|
|
247
216
|
console.log('\nDone. Run `sdoc setup` any time to revisit.');
|
|
248
217
|
}
|
|
249
218
|
|
|
250
|
-
// Auto-refresh
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
219
|
+
// Auto-refresh when the binary version is newer than the version that last
|
|
220
|
+
// ran. No prompt: the user already consented during setup. Rewrites only the
|
|
221
|
+
// canonical skill (every symlink follows); re-checks symlinks and re-strips
|
|
222
|
+
// any block that reappeared. Bails on downgrades or errors.
|
|
254
223
|
async function maybeAutoRefresh() {
|
|
255
224
|
if (process.env.SDOCS_NO_REFRESH) return;
|
|
256
225
|
let state = readSetupState();
|
|
257
226
|
|
|
258
|
-
// Implicit-consent migration for users who have
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// hand-edited it doesn't get state silently created.
|
|
227
|
+
// Implicit-consent migration for users who have evidence of a prior setup
|
|
228
|
+
// (a skill file on disk, or a recognised always-on block in one of the
|
|
229
|
+
// historical config files) but no ~/.sdocs/setup.json. A brand-new user has
|
|
230
|
+
// no evidence and is left for the interactive first-run prompt instead, so
|
|
231
|
+
// nothing is auto-installed without consent.
|
|
264
232
|
if (!state) {
|
|
265
|
-
|
|
266
|
-
const
|
|
233
|
+
if (!hasSetupEvidence(os.homedir(), process.env)) return;
|
|
234
|
+
const result = syncAgentSkill({});
|
|
235
|
+
if (result.errors.length) { printSyncSummary(result); return; }
|
|
236
|
+
if (!syncChanged(result)) return;
|
|
237
|
+
const next = implicitConsentState(toImplicitResults(result), VERSION);
|
|
267
238
|
if (!next) return;
|
|
268
|
-
|
|
239
|
+
printSyncSummary(result);
|
|
269
240
|
writeSetupState(next);
|
|
270
241
|
return;
|
|
271
242
|
}
|
|
@@ -273,53 +244,37 @@ async function maybeAutoRefresh() {
|
|
|
273
244
|
if (!state.autoRefreshAgentFiles) return;
|
|
274
245
|
if (compareVersions(VERSION, state.lastRunVersion) <= 0) return;
|
|
275
246
|
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
if (anyChanged) printRefreshSummary(results);
|
|
247
|
+
const result = syncAgentSkill({});
|
|
248
|
+
if (syncChanged(result) || result.errors.length) printSyncSummary(result);
|
|
279
249
|
|
|
280
|
-
|
|
281
|
-
if (!anyError) {
|
|
250
|
+
if (!result.errors.length) {
|
|
282
251
|
writeSetupState({ ...state, lastRunVersion: VERSION });
|
|
283
252
|
}
|
|
284
253
|
}
|
|
285
254
|
|
|
286
|
-
// `sdoc refresh`
|
|
287
|
-
//
|
|
288
|
-
// for agents that want to trigger the migration explicitly without going
|
|
289
|
-
// through the interactive setup flow.
|
|
255
|
+
// `sdoc refresh` - unconditional skill refresh. Useful when setup.json was
|
|
256
|
+
// never written or has been deleted, or to force the migration explicitly.
|
|
290
257
|
async function runRefresh() {
|
|
291
258
|
const existing = readSetupState();
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
const errors = results.filter(r => r.error);
|
|
295
|
-
const current = results.filter(r => r.reason === 'current');
|
|
296
|
-
const blocksPresent = changed.length + current.length;
|
|
297
|
-
|
|
298
|
-
printRefreshSummary(results);
|
|
299
|
-
|
|
300
|
-
if (changed.length === 0 && errors.length === 0) {
|
|
301
|
-
if (blocksPresent === 0) {
|
|
302
|
-
console.log('No SDocs blocks found in any agent file. Run `sdoc setup` to add one.');
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
console.log(`All SDocs agent blocks already at v${AGENT_BLOCK_VERSION}.`);
|
|
306
|
-
}
|
|
259
|
+
const result = syncAgentSkill({});
|
|
260
|
+
printSyncSummary(result);
|
|
307
261
|
|
|
308
|
-
if (errors.length
|
|
309
|
-
|
|
310
|
-
|
|
262
|
+
if (!syncChanged(result) && !result.errors.length) {
|
|
263
|
+
console.log(`SmallDocs skill already at v${SKILL_VERSION}.`);
|
|
264
|
+
}
|
|
265
|
+
if (result.errors.length) return;
|
|
311
266
|
|
|
312
267
|
writeSetupState({
|
|
313
|
-
setupCompleted: existing
|
|
314
|
-
writtenTo: [
|
|
268
|
+
setupCompleted: existing && existing.setupCompleted || new Date().toISOString(),
|
|
269
|
+
writtenTo: [canonicalSkillFile(os.homedir())],
|
|
315
270
|
declined: false,
|
|
316
271
|
autoRefreshAgentFiles: existing ? existing.autoRefreshAgentFiles !== false : true,
|
|
317
|
-
autoInstallUpdates: existing
|
|
272
|
+
autoInstallUpdates: existing && existing.autoInstallUpdates != null ? existing.autoInstallUpdates : false,
|
|
318
273
|
lastRunVersion: VERSION,
|
|
319
274
|
});
|
|
320
275
|
}
|
|
321
276
|
|
|
322
|
-
// `sdoc auto-update on|off|status`
|
|
277
|
+
// `sdoc auto-update on|off|status` - flips state.autoInstallUpdates.
|
|
323
278
|
function runAutoUpdateSubcommand(arg) {
|
|
324
279
|
let state = readSetupState();
|
|
325
280
|
if (!state) {
|
|
@@ -328,12 +283,12 @@ function runAutoUpdateSubcommand(arg) {
|
|
|
328
283
|
}
|
|
329
284
|
if (arg === 'on') {
|
|
330
285
|
writeSetupState({ ...state, autoInstallUpdates: true });
|
|
331
|
-
console.log('
|
|
286
|
+
console.log('\u2713 Auto-install of sdoc updates: on');
|
|
332
287
|
return;
|
|
333
288
|
}
|
|
334
289
|
if (arg === 'off') {
|
|
335
290
|
writeSetupState({ ...state, autoInstallUpdates: false });
|
|
336
|
-
console.log('
|
|
291
|
+
console.log('\u2713 Auto-install of sdoc updates: off');
|
|
337
292
|
return;
|
|
338
293
|
}
|
|
339
294
|
console.log(`Auto-install of sdoc updates: ${state.autoInstallUpdates ? 'on' : 'off'}`);
|
|
@@ -344,6 +299,7 @@ module.exports = {
|
|
|
344
299
|
ask,
|
|
345
300
|
askAutoInstallConsent,
|
|
346
301
|
askAutoRefreshConsent,
|
|
302
|
+
dryRunPreview,
|
|
347
303
|
runSetup,
|
|
348
304
|
runRefresh,
|
|
349
305
|
runAutoUpdateSubcommand,
|