sdocs-dev 1.15.0 → 1.19.2
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 +52 -17
- package/lib/agent-block.js +270 -50
- package/lib/agent-files.js +312 -74
- package/lib/cells-verify.js +1 -0
- package/lib/cloud-bindings.js +85 -0
- package/lib/cloud-commands.js +826 -0
- package/lib/cloud-credentials.js +380 -0
- package/lib/commands.js +46 -5
- package/lib/help-text.js +301 -70
- package/lib/io.js +53 -5
- 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 +182 -191
- package/lib/short-link.js +38 -1
- 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/setup.js
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
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
|
+
readSkillEdition,
|
|
24
|
+
canonicalSkillFile,
|
|
25
|
+
canonicalSkillDir,
|
|
26
|
+
legacyBlockTargets,
|
|
27
|
+
findBookendedBlock,
|
|
28
|
+
findLegacyBlock,
|
|
22
29
|
compareVersions,
|
|
23
30
|
readSetupState,
|
|
24
31
|
writeSetupState,
|
|
@@ -28,11 +35,12 @@ const {
|
|
|
28
35
|
const { upgradeCommand } = require('./update-check');
|
|
29
36
|
|
|
30
37
|
const {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
detectSkillAgents,
|
|
39
|
+
hasSetupEvidence,
|
|
40
|
+
syncAgentSkill,
|
|
41
|
+
syncChanged,
|
|
42
|
+
toImplicitResults,
|
|
43
|
+
printSyncSummary,
|
|
36
44
|
} = require('./agent-files');
|
|
37
45
|
|
|
38
46
|
const { VERSION, AGENT_CHANGES_URL } = require('./constants');
|
|
@@ -59,19 +67,82 @@ async function askAutoInstallConsent() {
|
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
async function askAutoRefreshConsent() {
|
|
62
|
-
console.log('\nKeep this
|
|
70
|
+
console.log('\nKeep this skill updated on future sdoc upgrades?');
|
|
63
71
|
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.');
|
|
72
|
+
console.log('When sdoc adds a feature we sometimes update the skill so your');
|
|
73
|
+
console.log('agent learns about it. Each change prints a notice with a link to');
|
|
74
|
+
console.log(`${AGENT_CHANGES_URL} showing the exact delta - the new wording, and why.`);
|
|
68
75
|
console.log('');
|
|
69
76
|
console.log('Re-run `sdoc setup` any time to change this.\n');
|
|
70
77
|
const a = await ask('Enable? [Y/n] ');
|
|
71
78
|
return !a || a === 'y' || a === 'yes';
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
// Preview what setup would do: print the skill, the symlinks it would create,
|
|
82
|
+
// the agents covered by the canonical copy, and any legacy blocks it would
|
|
83
|
+
// strip. Touches no file and writes no state.
|
|
84
|
+
function dryRunPreview(edition) {
|
|
85
|
+
const home = os.homedir();
|
|
86
|
+
const env = process.env;
|
|
87
|
+
edition = edition || installedEdition(home);
|
|
88
|
+
const skillPath = canonicalSkillFile(home);
|
|
89
|
+
console.log(`--- ${skillPath} ---`);
|
|
90
|
+
console.log(formatSkill(SKILL_VERSION, { cloud: edition === 'cloud' }));
|
|
91
|
+
|
|
92
|
+
const detected = detectSkillAgents(home, env);
|
|
93
|
+
const linked = detected.filter(a => !a.universal);
|
|
94
|
+
const universal = detected.filter(a => a.universal);
|
|
95
|
+
|
|
96
|
+
if (universal.length) {
|
|
97
|
+
console.log('\nCovered by the canonical copy (~/.agents/skills, no symlink needed):');
|
|
98
|
+
for (const a of universal) console.log(` ${a.displayName}`);
|
|
99
|
+
}
|
|
100
|
+
if (linked.length) {
|
|
101
|
+
console.log('\nSymlinks to create (<agent skills dir> -> canonical):');
|
|
102
|
+
for (const a of linked) console.log(` ${path.join(a.dir, SKILL_NAME)} -> ${canonicalSkillDir(home)}`);
|
|
103
|
+
}
|
|
104
|
+
if (detected.length === 0) {
|
|
105
|
+
console.log('\nNo coding-agent configs detected. The canonical skill is still written');
|
|
106
|
+
console.log('so any agent that discovers ~/.agents/skills picks it up.');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const wouldStrip = [];
|
|
110
|
+
for (const t of legacyBlockTargets(home, env)) {
|
|
111
|
+
let content;
|
|
112
|
+
try { content = fs.readFileSync(t.file, 'utf-8'); } catch (_) { continue; }
|
|
113
|
+
if (findBookendedBlock(content) || findLegacyBlock(content)) wouldStrip.push(t.file);
|
|
114
|
+
}
|
|
115
|
+
if (wouldStrip.length) {
|
|
116
|
+
console.log('\nLegacy SmallDocs blocks to remove:');
|
|
117
|
+
for (const f of wouldStrip) console.log(` ${f}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function installedEdition(home) {
|
|
122
|
+
try { return readSkillEdition(fs.readFileSync(canonicalSkillFile(home), 'utf8')); }
|
|
123
|
+
catch (_) { return 'standard'; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function setupStateFields(edition, accountId, existing) {
|
|
127
|
+
const activeEdition = edition || installedEdition(os.homedir());
|
|
128
|
+
return {
|
|
129
|
+
skillEdition: activeEdition,
|
|
130
|
+
cloudAccountId: activeEdition === 'cloud'
|
|
131
|
+
? (accountId || existing && existing.cloudAccountId || null) : null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function cloudFirstSettings() {
|
|
136
|
+
const state = readSetupState();
|
|
137
|
+
if (state) {
|
|
138
|
+
if (state.declined || state.skillEdition !== 'cloud') return null;
|
|
139
|
+
return { accountId: state.cloudAccountId || null };
|
|
140
|
+
}
|
|
141
|
+
return installedEdition(os.homedir()) === 'cloud' ? { accountId: null } : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function runSetup({ force = false, yes = false, dryRun = false,
|
|
145
|
+
edition = null, accountId = null } = {}) {
|
|
75
146
|
if (!force) {
|
|
76
147
|
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
77
148
|
if (process.env.CI || process.env.SDOCS_NO_SETUP) return;
|
|
@@ -79,143 +150,71 @@ async function runSetup({ force = false, yes = false, dryRun = false } = {}) {
|
|
|
79
150
|
}
|
|
80
151
|
|
|
81
152
|
// ── --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
153
|
if (yes) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
}
|
|
154
|
+
if (dryRun) { dryRunPreview(edition); return; }
|
|
155
|
+
|
|
156
|
+
const existing = readSetupState();
|
|
157
|
+
const result = syncAgentSkill({ edition });
|
|
158
|
+
const changed = syncChanged(result);
|
|
159
|
+
const detected = detectSkillAgents(os.homedir(), process.env);
|
|
105
160
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// on a stale install.
|
|
109
|
-
const refreshResults = refreshAllAgentFiles();
|
|
110
|
-
const refreshedFiles = refreshResults.filter(r => r.changed).map(r => r.path);
|
|
111
|
-
if (refreshResults.some(r => r.changed)) printRefreshSummary(refreshResults);
|
|
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}`); }
|
|
161
|
+
if (changed || result.errors.length) {
|
|
162
|
+
printSyncSummary(result);
|
|
121
163
|
}
|
|
164
|
+
if (result.errors.length) return;
|
|
122
165
|
|
|
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.');
|
|
166
|
+
if (!changed) {
|
|
167
|
+
if (detected.length > 0) {
|
|
168
|
+
console.log('SmallDocs skill already at current version. Nothing to do.');
|
|
136
169
|
} else {
|
|
137
|
-
console.log('No coding-agent configs detected.
|
|
138
|
-
console.log('
|
|
170
|
+
console.log('No coding-agent configs detected. The canonical skill is at');
|
|
171
|
+
console.log('~/.agents/skills/smalldocs/SKILL.md; any agent that discovers');
|
|
172
|
+
console.log('~/.agents/skills will pick it up.');
|
|
139
173
|
}
|
|
140
|
-
return;
|
|
141
174
|
}
|
|
142
175
|
|
|
143
176
|
writeSetupState({
|
|
144
177
|
setupCompleted: new Date().toISOString(),
|
|
145
|
-
writtenTo:
|
|
178
|
+
writtenTo: changed ? [canonicalSkillFile(os.homedir())] : [],
|
|
179
|
+
declined: false,
|
|
146
180
|
autoRefreshAgentFiles: true,
|
|
147
181
|
autoInstallUpdates: false,
|
|
148
182
|
lastRunVersion: VERSION,
|
|
183
|
+
...setupStateFields(edition, accountId, existing),
|
|
149
184
|
});
|
|
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
185
|
return;
|
|
156
186
|
}
|
|
157
187
|
|
|
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;
|
|
188
|
+
// ── interactive path ───────────────────────────────────────
|
|
189
|
+
const home = os.homedir();
|
|
190
|
+
const detected = detectSkillAgents(home, process.env);
|
|
191
|
+
|
|
192
|
+
console.log('\n\u2728─────── SmallDocs setup ───────\u2728');
|
|
193
|
+
console.log('Install the SmallDocs skill so your coding agents know `sdoc`.\n');
|
|
194
|
+
|
|
195
|
+
if (detected.length > 0) {
|
|
196
|
+
console.log('Detected: ' + detected.map(a => a.displayName).join(', '));
|
|
197
|
+
console.log('\nWill write the skill to ~/.agents/skills/smalldocs/SKILL.md.');
|
|
198
|
+
console.log('Agents using that universal location read it directly; other');
|
|
199
|
+
console.log('detected agents receive a symlink in their skills directory.');
|
|
200
|
+
} else {
|
|
201
|
+
console.log('No coding-agent configs detected. Setup still writes the canonical');
|
|
202
|
+
console.log('skill at ~/.agents/skills/smalldocs/SKILL.md, which any agent that');
|
|
203
|
+
console.log('discovers ~/.agents/skills will pick up.');
|
|
197
204
|
}
|
|
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:');
|
|
205
|
+
console.log('\nYou can ask your agent things like:');
|
|
208
206
|
console.log(' "write up the plan and sdoc it to me"');
|
|
209
207
|
console.log(' "explain async/await to me in a sdoc"');
|
|
210
208
|
console.log(' "draft the release notes as a sdoc I can share"');
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
console.log(
|
|
209
|
+
|
|
210
|
+
const RULE = '\u2550'.repeat(36);
|
|
211
|
+
console.log(`\n${RULE} Skill body ${RULE}`);
|
|
212
|
+
const displayEdition = edition || installedEdition(home);
|
|
213
|
+
console.log(formatSkill(SKILL_VERSION, { cloud: displayEdition === 'cloud' })
|
|
214
|
+
.replace(/^---[\s\S]*?---\n\n<!--[^\n]+-->\n<!--[^\n]+-->\n/, '').trim());
|
|
216
215
|
console.log(RULE);
|
|
217
216
|
|
|
218
|
-
const a = await ask('\
|
|
217
|
+
const a = await ask('\nInstall? [Y/n/skip] ');
|
|
219
218
|
const skipped = a === 'skip' || (a && a !== 'y' && a !== 'yes');
|
|
220
219
|
if (skipped) {
|
|
221
220
|
writeSetupState({
|
|
@@ -223,103 +222,93 @@ async function runSetup({ force = false, yes = false, dryRun = false } = {}) {
|
|
|
223
222
|
writtenTo: [], declined: true,
|
|
224
223
|
autoRefreshAgentFiles: false, autoInstallUpdates: false,
|
|
225
224
|
lastRunVersion: VERSION,
|
|
225
|
+
...setupStateFields(edition, accountId, readSetupState()),
|
|
226
226
|
});
|
|
227
227
|
console.log('Skipped. Run `sdoc setup` any time to revisit.');
|
|
228
228
|
return;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
231
|
+
const existing = readSetupState();
|
|
232
|
+
const result = syncAgentSkill({ edition });
|
|
233
|
+
const changed = syncChanged(result);
|
|
234
|
+
if (changed || result.errors.length) printSyncSummary(result);
|
|
235
|
+
if (result.errors.length) return;
|
|
236
236
|
|
|
237
|
-
const autoRefresh =
|
|
238
|
-
const autoInstall =
|
|
237
|
+
const autoRefresh = await askAutoRefreshConsent();
|
|
238
|
+
const autoInstall = await askAutoInstallConsent();
|
|
239
239
|
|
|
240
240
|
writeSetupState({
|
|
241
241
|
setupCompleted: new Date().toISOString(),
|
|
242
|
-
writtenTo
|
|
242
|
+
writtenTo: changed ? [canonicalSkillFile(home)] : [],
|
|
243
|
+
declined: false,
|
|
243
244
|
autoRefreshAgentFiles: autoRefresh,
|
|
244
245
|
autoInstallUpdates: autoInstall,
|
|
245
246
|
lastRunVersion: VERSION,
|
|
247
|
+
...setupStateFields(edition, accountId, existing),
|
|
246
248
|
});
|
|
247
249
|
console.log('\nDone. Run `sdoc setup` any time to revisit.');
|
|
248
250
|
}
|
|
249
251
|
|
|
250
|
-
// Auto-refresh
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
252
|
+
// Auto-refresh when the binary version is newer than the version that last
|
|
253
|
+
// ran. No prompt: the user already consented during setup. Rewrites only the
|
|
254
|
+
// canonical skill (every symlink follows); re-checks symlinks and re-strips
|
|
255
|
+
// any block that reappeared. Bails on downgrades or errors.
|
|
254
256
|
async function maybeAutoRefresh() {
|
|
255
257
|
if (process.env.SDOCS_NO_REFRESH) return;
|
|
256
258
|
let state = readSetupState();
|
|
257
259
|
|
|
258
|
-
// Implicit-consent migration for users who have
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// hand-edited it doesn't get state silently created.
|
|
260
|
+
// Implicit-consent migration for users who have evidence of a prior setup
|
|
261
|
+
// (a skill file on disk, or a recognised always-on block in one of the
|
|
262
|
+
// historical config files) but no ~/.sdocs/setup.json. A brand-new user has
|
|
263
|
+
// no evidence and is left for the interactive first-run prompt instead, so
|
|
264
|
+
// nothing is auto-installed without consent.
|
|
264
265
|
if (!state) {
|
|
265
|
-
|
|
266
|
-
const
|
|
266
|
+
if (!hasSetupEvidence(os.homedir(), process.env)) return;
|
|
267
|
+
const result = syncAgentSkill({});
|
|
268
|
+
if (result.errors.length) { printSyncSummary(result); return; }
|
|
269
|
+
if (!syncChanged(result)) return;
|
|
270
|
+
const next = implicitConsentState(toImplicitResults(result), VERSION);
|
|
267
271
|
if (!next) return;
|
|
268
|
-
|
|
269
|
-
writeSetupState(next);
|
|
272
|
+
printSyncSummary(result);
|
|
273
|
+
writeSetupState({ ...next, ...setupStateFields(null, null, null) });
|
|
270
274
|
return;
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
if (!state.autoRefreshAgentFiles) return;
|
|
274
278
|
if (compareVersions(VERSION, state.lastRunVersion) <= 0) return;
|
|
275
279
|
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
if (anyChanged) printRefreshSummary(results);
|
|
280
|
+
const result = syncAgentSkill({});
|
|
281
|
+
if (syncChanged(result) || result.errors.length) printSyncSummary(result);
|
|
279
282
|
|
|
280
|
-
|
|
281
|
-
if (!anyError) {
|
|
283
|
+
if (!result.errors.length) {
|
|
282
284
|
writeSetupState({ ...state, lastRunVersion: VERSION });
|
|
283
285
|
}
|
|
284
286
|
}
|
|
285
287
|
|
|
286
|
-
// `sdoc refresh`
|
|
287
|
-
//
|
|
288
|
-
// for agents that want to trigger the migration explicitly without going
|
|
289
|
-
// through the interactive setup flow.
|
|
288
|
+
// `sdoc refresh` - unconditional skill refresh. Useful when setup.json was
|
|
289
|
+
// never written or has been deleted, or to force the migration explicitly.
|
|
290
290
|
async function runRefresh() {
|
|
291
291
|
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
|
-
}
|
|
292
|
+
const result = syncAgentSkill({});
|
|
293
|
+
printSyncSummary(result);
|
|
307
294
|
|
|
308
|
-
if (errors.length
|
|
309
|
-
|
|
310
|
-
|
|
295
|
+
if (!syncChanged(result) && !result.errors.length) {
|
|
296
|
+
console.log(`SmallDocs skill already at v${SKILL_VERSION}.`);
|
|
297
|
+
}
|
|
298
|
+
if (result.errors.length) return;
|
|
311
299
|
|
|
312
300
|
writeSetupState({
|
|
313
|
-
setupCompleted: existing
|
|
314
|
-
writtenTo: [
|
|
301
|
+
setupCompleted: existing && existing.setupCompleted || new Date().toISOString(),
|
|
302
|
+
writtenTo: [canonicalSkillFile(os.homedir())],
|
|
315
303
|
declined: false,
|
|
316
304
|
autoRefreshAgentFiles: existing ? existing.autoRefreshAgentFiles !== false : true,
|
|
317
|
-
autoInstallUpdates: existing
|
|
305
|
+
autoInstallUpdates: existing && existing.autoInstallUpdates != null ? existing.autoInstallUpdates : false,
|
|
318
306
|
lastRunVersion: VERSION,
|
|
307
|
+
...setupStateFields(null, null, existing),
|
|
319
308
|
});
|
|
320
309
|
}
|
|
321
310
|
|
|
322
|
-
// `sdoc auto-update on|off|status`
|
|
311
|
+
// `sdoc auto-update on|off|status` - flips state.autoInstallUpdates.
|
|
323
312
|
function runAutoUpdateSubcommand(arg) {
|
|
324
313
|
let state = readSetupState();
|
|
325
314
|
if (!state) {
|
|
@@ -328,12 +317,12 @@ function runAutoUpdateSubcommand(arg) {
|
|
|
328
317
|
}
|
|
329
318
|
if (arg === 'on') {
|
|
330
319
|
writeSetupState({ ...state, autoInstallUpdates: true });
|
|
331
|
-
console.log('
|
|
320
|
+
console.log('\u2713 Auto-install of sdoc updates: on');
|
|
332
321
|
return;
|
|
333
322
|
}
|
|
334
323
|
if (arg === 'off') {
|
|
335
324
|
writeSetupState({ ...state, autoInstallUpdates: false });
|
|
336
|
-
console.log('
|
|
325
|
+
console.log('\u2713 Auto-install of sdoc updates: off');
|
|
337
326
|
return;
|
|
338
327
|
}
|
|
339
328
|
console.log(`Auto-install of sdoc updates: ${state.autoInstallUpdates ? 'on' : 'off'}`);
|
|
@@ -344,8 +333,10 @@ module.exports = {
|
|
|
344
333
|
ask,
|
|
345
334
|
askAutoInstallConsent,
|
|
346
335
|
askAutoRefreshConsent,
|
|
336
|
+
dryRunPreview,
|
|
347
337
|
runSetup,
|
|
348
338
|
runRefresh,
|
|
349
339
|
runAutoUpdateSubcommand,
|
|
350
340
|
maybeAutoRefresh,
|
|
341
|
+
cloudFirstSettings,
|
|
351
342
|
};
|
package/lib/short-link.js
CHANGED
|
@@ -18,6 +18,35 @@ const { slugify } = require('../shared/sdocs-slugify.js');
|
|
|
18
18
|
const { toBase64Url } = require('./url');
|
|
19
19
|
const { DEFAULT_URL } = require('./constants');
|
|
20
20
|
|
|
21
|
+
const PLACEMENT_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{3,31}$/;
|
|
22
|
+
const PLACEMENT_SOURCES = new Set(['x', 'yt', 'youtube']);
|
|
23
|
+
|
|
24
|
+
function normalizePlacementId(value) {
|
|
25
|
+
if (value == null || value === '') return '';
|
|
26
|
+
const placement = String(value).trim();
|
|
27
|
+
if (!PLACEMENT_RE.test(placement)) {
|
|
28
|
+
throw new Error('placement must be 4-32 letters, numbers, underscores, or hyphens');
|
|
29
|
+
}
|
|
30
|
+
return placement;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildAttributionQuery(sourceValue, placementValue) {
|
|
34
|
+
const source = String(sourceValue || '').trim().toLowerCase();
|
|
35
|
+
const placement = normalizePlacementId(placementValue);
|
|
36
|
+
if (placement && !PLACEMENT_SOURCES.has(source)) {
|
|
37
|
+
throw new Error('placement requires --source x, yt, or youtube');
|
|
38
|
+
}
|
|
39
|
+
const query = new URLSearchParams();
|
|
40
|
+
if (source) query.set('src', source);
|
|
41
|
+
if (placement) query.set('pid', placement);
|
|
42
|
+
return query.toString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assembleShortUrl(baseUrl, id, attributionQuery, fragmentParams) {
|
|
46
|
+
const sourceQuery = attributionQuery ? `?${attributionQuery}` : '';
|
|
47
|
+
return `${baseUrl}/s/${id}${sourceQuery}#${fragmentParams.toString()}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
21
50
|
// The blob format (nonce(12) + ciphertext + tag(16)) matches the browser.
|
|
22
51
|
function compressAndEncrypt(content) {
|
|
23
52
|
const compressed = zlib.brotliCompressSync(Buffer.from(content, 'utf-8'), {
|
|
@@ -72,6 +101,8 @@ function uploadShortLink(ciphertextB64, baseUrl) {
|
|
|
72
101
|
|
|
73
102
|
async function buildShortUrl(content, opts) {
|
|
74
103
|
if (!content) throw new Error('short link requires file content');
|
|
104
|
+
opts = opts || {};
|
|
105
|
+
const attributionQuery = buildAttributionQuery(opts.source, opts.placement);
|
|
75
106
|
|
|
76
107
|
// Mirror the hash-build's default-stripping so the encrypted payload is
|
|
77
108
|
// identical to what the browser would encode.
|
|
@@ -95,11 +126,17 @@ async function buildShortUrl(content, opts) {
|
|
|
95
126
|
if (opts.theme) params.set('theme', opts.theme);
|
|
96
127
|
if (opts.section) params.set('sec', slugify(opts.section));
|
|
97
128
|
|
|
98
|
-
|
|
129
|
+
// Source attribution belongs in the query so the server can receive it. The
|
|
130
|
+
// encryption key stays after # and therefore never leaves the browser. The
|
|
131
|
+
// short-link id and encrypted payload are unchanged by this label.
|
|
132
|
+
return assembleShortUrl(baseUrl, id, attributionQuery, params);
|
|
99
133
|
}
|
|
100
134
|
|
|
101
135
|
module.exports = {
|
|
102
136
|
compressAndEncrypt,
|
|
103
137
|
uploadShortLink,
|
|
104
138
|
buildShortUrl,
|
|
139
|
+
normalizePlacementId,
|
|
140
|
+
buildAttributionQuery,
|
|
141
|
+
assembleShortUrl,
|
|
105
142
|
};
|