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/agent-files.js
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
|
-
//
|
|
1
|
+
// SmallDocs skill install + legacy-block migration.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// One canonical SKILL.md lives at ~/.agents/skills/smalldocs/SKILL.md.
|
|
4
|
+
// Every other supported agent gets a relative symlink from its own skills
|
|
5
|
+
// directory into the canonical dir (single source of truth; one write
|
|
6
|
+
// updates every agent). Universal agents already discover ~/.agents/skills,
|
|
7
|
+
// so they are skipped. Windows falls back to a junction (absolute target),
|
|
8
|
+
// then a copy if symlinks are unavailable.
|
|
9
|
+
//
|
|
10
|
+
// On setup/refresh we also strip any recognised always-on block from the
|
|
11
|
+
// historical agent config files so the reference is not loaded twice.
|
|
7
12
|
|
|
8
13
|
const fs = require('fs');
|
|
9
14
|
const os = require('os');
|
|
10
15
|
const path = require('path');
|
|
11
16
|
|
|
12
17
|
const {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
SKILL_VERSION,
|
|
19
|
+
SKILL_NAME,
|
|
20
|
+
formatSkill,
|
|
21
|
+
readSkillVersion,
|
|
22
|
+
readSkillEdition,
|
|
23
|
+
canonicalSkillDir,
|
|
24
|
+
canonicalSkillFile,
|
|
25
|
+
resolveSkillAgents,
|
|
26
|
+
legacyBlockTargets,
|
|
18
27
|
findBookendedBlock,
|
|
19
|
-
|
|
28
|
+
findLegacyBlock,
|
|
29
|
+
removeBlockContent,
|
|
20
30
|
} = require('./agent-block');
|
|
21
31
|
|
|
22
32
|
const { AGENT_CHANGES_URL } = require('./constants');
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
const home = os.homedir();
|
|
26
|
-
return AGENT_TARGETS
|
|
27
|
-
.map(t => ({ ...t, dirPath: path.join(home, t.detectDir || t.dir), filePath: path.join(home, t.dir, t.file) }))
|
|
28
|
-
.filter(t => fs.existsSync(t.dirPath));
|
|
29
|
-
}
|
|
34
|
+
const IS_WIN = process.platform === 'win32';
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
-
return findBookendedBlock(content) !== null
|
|
35
|
-
|| content.includes(AGENT_BLOCK_LEGACY_OPEN);
|
|
36
|
-
} catch (_) { return false; }
|
|
37
|
-
}
|
|
36
|
+
// ── generic file helpers ───────────────────────────────────
|
|
38
37
|
|
|
39
38
|
function isSymlink(filePath) {
|
|
40
39
|
try { return fs.lstatSync(filePath).isSymbolicLink(); }
|
|
@@ -84,79 +83,318 @@ function acquireLock(filePath) {
|
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
function
|
|
88
|
-
fs.mkdirSync(
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
function copyDirRecursive(src, dest) {
|
|
87
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
88
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
89
|
+
const s = path.join(src, entry.name);
|
|
90
|
+
const d = path.join(dest, entry.name);
|
|
91
|
+
if (entry.isDirectory()) copyDirRecursive(s, d);
|
|
92
|
+
else fs.copyFileSync(s, d);
|
|
93
93
|
}
|
|
94
|
-
const existing = fs.readFileSync(filePath, 'utf-8');
|
|
95
|
-
const prefix = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
96
|
-
atomicWrite(filePath, existing + prefix + block);
|
|
97
94
|
}
|
|
98
95
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
// Compare filesystem identity, not just path spelling. Agent skill parents
|
|
97
|
+
// are sometimes symlinked to ~/.agents/skills; in that case two different
|
|
98
|
+
// path strings can name the exact same skill directory.
|
|
99
|
+
function pathsResolveSame(a, b) {
|
|
100
|
+
try { return fs.realpathSync(a) === fs.realpathSync(b); }
|
|
101
|
+
catch (_) {}
|
|
102
|
+
|
|
103
|
+
function withRealParent(p) {
|
|
104
|
+
const resolved = path.resolve(p);
|
|
105
|
+
try { return path.join(fs.realpathSync(path.dirname(resolved)), path.basename(resolved)); }
|
|
106
|
+
catch (_) { return resolved; }
|
|
107
|
+
}
|
|
108
|
+
return withRealParent(a) === withRealParent(b);
|
|
109
|
+
}
|
|
104
110
|
|
|
105
|
-
|
|
106
|
-
if (!release) return { path: filePath, changed: false, reason: 'locked' };
|
|
111
|
+
// ── canonical skill ────────────────────────────────────────
|
|
107
112
|
|
|
113
|
+
function refreshCanonicalSkill(home, opts = {}) {
|
|
114
|
+
home = home || os.homedir();
|
|
115
|
+
const file = canonicalSkillFile(home);
|
|
116
|
+
let existing = null;
|
|
108
117
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
const st = fs.lstatSync(file);
|
|
119
|
+
if (!st.isFile()) {
|
|
120
|
+
return {
|
|
121
|
+
changed: false, reason: 'conflict', path: file,
|
|
122
|
+
error: 'existing canonical SKILL.md is not a regular SmallDocs-managed file; left untouched',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
existing = fs.readFileSync(file, 'utf-8');
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (e.code !== 'ENOENT') {
|
|
128
|
+
return { changed: false, reason: 'error', path: file, error: e.message };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const currentVersion = existing ? readSkillVersion(existing) : null;
|
|
132
|
+
const currentEdition = existing ? readSkillEdition(existing) : 'standard';
|
|
133
|
+
const requestedEdition = opts.edition === 'cloud' || opts.edition === 'standard'
|
|
134
|
+
? opts.edition : null;
|
|
135
|
+
const desiredEdition = requestedEdition || currentEdition;
|
|
136
|
+
if (existing !== null && currentVersion === null) {
|
|
114
137
|
return {
|
|
115
|
-
|
|
116
|
-
|
|
138
|
+
changed: false, reason: 'conflict', path: file,
|
|
139
|
+
error: 'existing canonical SKILL.md is not managed by SmallDocs; left untouched',
|
|
117
140
|
};
|
|
141
|
+
}
|
|
142
|
+
if (currentVersion === SKILL_VERSION && currentEdition === desiredEdition) {
|
|
143
|
+
return { changed: false, reason: 'current', path: file };
|
|
144
|
+
}
|
|
145
|
+
if (currentVersion !== null && currentVersion > SKILL_VERSION) {
|
|
146
|
+
return { changed: false, reason: 'newer', path: file };
|
|
147
|
+
}
|
|
148
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
149
|
+
atomicWrite(file, formatSkill(SKILL_VERSION, { cloud: desiredEdition === 'cloud' }));
|
|
150
|
+
return {
|
|
151
|
+
changed: true, path: file,
|
|
152
|
+
fromVersion: currentVersion || 0, toVersion: SKILL_VERSION,
|
|
153
|
+
fromEdition: currentEdition, toEdition: desiredEdition,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── symlink install ────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
// A real directory at an agent skills path is only safe to replace if it looks
|
|
160
|
+
// like one of our own copy-fallback installs (a prior Windows run, or a system
|
|
161
|
+
// where symlinks are unavailable): it must contain a SKILL.md carrying our
|
|
162
|
+
// sdocs-skill marker. Anything else is user content we must not destroy.
|
|
163
|
+
function looksLikeOurSkillDir(dir) {
|
|
164
|
+
try {
|
|
165
|
+
const content = fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf-8');
|
|
166
|
+
return readSkillVersion(content) !== null;
|
|
167
|
+
} catch (_) { return false; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Ensure <linkDir> resolves to <canonicalDir>. Idempotent: a link already
|
|
171
|
+
// pointing at canonical is a no-op; a wrong/stale symlink is replaced. A real
|
|
172
|
+
// directory is replaced only if it is recognisably one of our copy-fallback
|
|
173
|
+
// installs. Other directories and files are left untouched (returns an error)
|
|
174
|
+
// so the user's own content is never clobbered. Returns { method } where
|
|
175
|
+
// method is symlink | copy | noop | error.
|
|
176
|
+
function ensureSkillLink(canonicalDir, linkDir) {
|
|
177
|
+
if (pathsResolveSame(canonicalDir, linkDir)) return { method: 'noop' };
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const st = fs.lstatSync(linkDir);
|
|
181
|
+
if (st.isSymbolicLink()) {
|
|
182
|
+
const tgt = fs.readlinkSync(linkDir);
|
|
183
|
+
const resolved = path.resolve(path.dirname(linkDir), tgt);
|
|
184
|
+
if (resolved === canonicalDir) return { method: 'noop' };
|
|
185
|
+
// Stale/wrong symlink: safe to remove (a link holds no user data).
|
|
186
|
+
fs.rmSync(linkDir, { recursive: true, force: true });
|
|
187
|
+
} else if (st.isDirectory()) {
|
|
188
|
+
if (!looksLikeOurSkillDir(linkDir)) {
|
|
189
|
+
return { method: 'error', error: 'exists and is not a SmallDocs skill directory; left untouched' };
|
|
190
|
+
}
|
|
191
|
+
fs.rmSync(linkDir, { recursive: true, force: true });
|
|
192
|
+
} else {
|
|
193
|
+
return { method: 'error', error: 'exists and is not a SmallDocs skill directory; left untouched' };
|
|
194
|
+
}
|
|
118
195
|
} catch (e) {
|
|
119
|
-
return {
|
|
120
|
-
}
|
|
121
|
-
|
|
196
|
+
if (e.code !== 'ENOENT') return { method: 'error', error: e.message };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try { fs.mkdirSync(path.dirname(linkDir), { recursive: true }); }
|
|
200
|
+
catch (e) { return { method: 'error', error: e.message }; }
|
|
201
|
+
|
|
202
|
+
// Relative symlink on POSIX so it survives a home-dir move; junction on
|
|
203
|
+
// Windows requires an absolute target.
|
|
204
|
+
try {
|
|
205
|
+
let target = path.resolve(canonicalDir);
|
|
206
|
+
let linkParent = path.resolve(path.dirname(linkDir));
|
|
207
|
+
try { target = fs.realpathSync(canonicalDir); } catch (_) {}
|
|
208
|
+
try { linkParent = fs.realpathSync(path.dirname(linkDir)); } catch (_) {}
|
|
209
|
+
if (IS_WIN) {
|
|
210
|
+
fs.symlinkSync(target, linkDir, 'junction');
|
|
211
|
+
} else {
|
|
212
|
+
const rel = path.relative(linkParent, target);
|
|
213
|
+
fs.symlinkSync(rel, linkDir, 'dir');
|
|
214
|
+
}
|
|
215
|
+
return { method: 'symlink' };
|
|
216
|
+
} catch (_) {
|
|
217
|
+
// Permission / non-admin Windows: fall back to a real copy.
|
|
218
|
+
try {
|
|
219
|
+
copyDirRecursive(canonicalDir, linkDir);
|
|
220
|
+
return { method: 'copy' };
|
|
221
|
+
} catch (e) {
|
|
222
|
+
return { method: 'error', error: e.message };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function detectSkillAgents(home, env) {
|
|
228
|
+
home = home || os.homedir();
|
|
229
|
+
return resolveSkillAgents(home, env || process.env)
|
|
230
|
+
.filter(a => a.detect.some(p => { try { return fs.existsSync(p); } catch (_) { return false; } }));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Evidence that the user previously opted into SmallDocs setup: a skill file
|
|
234
|
+
// already on disk (prior skill install), or a recognised always-on block in one
|
|
235
|
+
// of the historical config files (pre-skill install). maybeAutoRefresh uses
|
|
236
|
+
// this so a brand-new user is NOT auto-installed without consent - they get the
|
|
237
|
+
// interactive first-run prompt (or run `sdoc setup --yes`) instead.
|
|
238
|
+
function hasSetupEvidence(home, env) {
|
|
239
|
+
home = home || os.homedir();
|
|
240
|
+
env = env || process.env;
|
|
241
|
+
try {
|
|
242
|
+
const content = fs.readFileSync(canonicalSkillFile(home), 'utf-8');
|
|
243
|
+
if (readSkillVersion(content) !== null) return true;
|
|
244
|
+
} catch (_) {}
|
|
245
|
+
for (const t of legacyBlockTargets(home, env)) {
|
|
246
|
+
let content;
|
|
247
|
+
try { content = fs.readFileSync(t.file, 'utf-8'); } catch (_) { continue; }
|
|
248
|
+
if (findBookendedBlock(content) || findLegacyBlock(content)) return true;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── legacy block stripping ─────────────────────────────────
|
|
254
|
+
|
|
255
|
+
function stripLegacyBlocks(home, env) {
|
|
256
|
+
home = home || os.homedir();
|
|
257
|
+
const out = [];
|
|
258
|
+
|
|
259
|
+
function skippedTarget(t, reason) {
|
|
260
|
+
try {
|
|
261
|
+
const content = fs.readFileSync(t.file, 'utf-8');
|
|
262
|
+
const hasBlock = !!(findBookendedBlock(content) || findLegacyBlock(content));
|
|
263
|
+
return {
|
|
264
|
+
name: t.name, file: t.file, changed: false, reason,
|
|
265
|
+
error: hasBlock ? `${reason}; recognised legacy SmallDocs block left untouched` : undefined,
|
|
266
|
+
};
|
|
267
|
+
} catch (e) {
|
|
268
|
+
return { name: t.name, file: t.file, changed: false, reason, error: e.message };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const t of legacyBlockTargets(home, env || process.env)) {
|
|
273
|
+
if (!fs.existsSync(t.file)) { out.push({ name: t.name, file: t.file, changed: false, reason: 'absent' }); continue; }
|
|
274
|
+
if (isSymlink(t.file)) { out.push(skippedTarget(t, 'symlink')); continue; }
|
|
275
|
+
|
|
276
|
+
const release = acquireLock(t.file);
|
|
277
|
+
if (!release) { out.push(skippedTarget(t, 'locked')); continue; }
|
|
278
|
+
try {
|
|
279
|
+
const content = fs.readFileSync(t.file, 'utf-8');
|
|
280
|
+
const r = removeBlockContent(content);
|
|
281
|
+
if (!r.changed) { out.push({ name: t.name, file: t.file, changed: false, reason: r.reason }); continue; }
|
|
282
|
+
backupFile(t.file);
|
|
283
|
+
atomicWrite(t.file, r.content);
|
|
284
|
+
out.push({ name: t.name, file: t.file, changed: true, fromVersion: r.version });
|
|
285
|
+
} catch (e) {
|
|
286
|
+
out.push({ name: t.name, file: t.file, changed: false, error: e.message });
|
|
287
|
+
} finally {
|
|
288
|
+
release();
|
|
289
|
+
}
|
|
122
290
|
}
|
|
291
|
+
return out;
|
|
123
292
|
}
|
|
124
293
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
294
|
+
// ── orchestration ──────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
// One call that does the full sync: refresh canonical skill, symlink every
|
|
297
|
+
// detected non-universal agent, strip legacy blocks. Returns a result the
|
|
298
|
+
// setup flow reports and derives setup-state from.
|
|
299
|
+
function syncAgentSkill(opts = {}) {
|
|
300
|
+
const home = opts.home || os.homedir();
|
|
301
|
+
const env = opts.env || process.env;
|
|
302
|
+
const canonicalDir = canonicalSkillDir(home);
|
|
303
|
+
|
|
304
|
+
const result = {
|
|
305
|
+
canonical: refreshCanonicalSkill(home, { edition: opts.edition }),
|
|
306
|
+
links: [],
|
|
307
|
+
stripped: [],
|
|
308
|
+
errors: [],
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
if (result.canonical.error) {
|
|
312
|
+
result.errors.push(`${result.canonical.path}: ${result.canonical.error}`);
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const agent of detectSkillAgents(home, env)) {
|
|
317
|
+
if (agent.universal) continue; // canonical copy already covers them
|
|
318
|
+
const linkDir = path.join(agent.dir, SKILL_NAME);
|
|
319
|
+
const r = ensureSkillLink(canonicalDir, linkDir);
|
|
320
|
+
result.links.push({ name: agent.displayName, path: linkDir, ...r });
|
|
321
|
+
if (r.error) result.errors.push(`${agent.name}: ${r.error}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Keep the legacy instructions in place unless every required skill link
|
|
325
|
+
// succeeded. They are the safe fallback if an agent-specific path collides
|
|
326
|
+
// with user content or cannot be written.
|
|
327
|
+
if (result.errors.length) return result;
|
|
328
|
+
|
|
329
|
+
result.stripped = stripLegacyBlocks(home, env);
|
|
330
|
+
for (const s of result.stripped) {
|
|
331
|
+
if (s.error) result.errors.push(`${s.file}: ${s.error}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// True if a sync changed anything (skill written/upgraded, a link created,
|
|
338
|
+
// or a block stripped). Drives the "nothing to do" message and setup state.
|
|
339
|
+
function syncChanged(result) {
|
|
340
|
+
if (result.canonical && result.canonical.changed) return true;
|
|
341
|
+
if (result.links.some(l => l.method === 'symlink' || l.method === 'copy')) return true;
|
|
342
|
+
if (result.stripped.some(s => s.changed)) return true;
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Flatten a sync result into the {path, changed, error?} shape that
|
|
347
|
+
// implicitConsentState expects.
|
|
348
|
+
function toImplicitResults(result) {
|
|
349
|
+
const arr = [];
|
|
350
|
+
if (result.canonical) {
|
|
351
|
+
arr.push({ path: result.canonical.path, changed: !!result.canonical.changed, error: result.canonical.error });
|
|
352
|
+
}
|
|
353
|
+
for (const l of result.links) {
|
|
354
|
+
arr.push({ path: l.path, changed: l.method === 'symlink' || l.method === 'copy', error: l.error });
|
|
355
|
+
}
|
|
356
|
+
for (const s of result.stripped) {
|
|
357
|
+
arr.push({ path: s.file, changed: s.changed, error: s.error });
|
|
358
|
+
}
|
|
359
|
+
return arr;
|
|
131
360
|
}
|
|
132
361
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
console.log(`✓ SDocs agent block updated to v${AGENT_BLOCK_VERSION} in ${n} ${n === 1 ? 'file' : 'files'}`);
|
|
138
|
-
console.log(` Changes: ${AGENT_CHANGES_URL}#v${AGENT_BLOCK_VERSION}`);
|
|
362
|
+
function printSyncSummary(result) {
|
|
363
|
+
if (result.canonical && result.canonical.changed) {
|
|
364
|
+
console.log(`\u2713 SmallDocs skill updated to v${SKILL_VERSION} at ${result.canonical.path}`);
|
|
365
|
+
console.log(` Changes: ${AGENT_CHANGES_URL}#v${SKILL_VERSION}`);
|
|
139
366
|
}
|
|
140
|
-
|
|
141
|
-
console.log(`! ${
|
|
367
|
+
if (result.canonical && result.canonical.error) {
|
|
368
|
+
console.log(`! ${result.canonical.path}: ${result.canonical.error}`);
|
|
142
369
|
}
|
|
143
|
-
for (const
|
|
144
|
-
|
|
370
|
+
for (const l of result.links) {
|
|
371
|
+
if (l.method === 'symlink' || l.method === 'copy') {
|
|
372
|
+
console.log(`\u2713 ${l.name}: ${l.path} (${l.method})`);
|
|
373
|
+
}
|
|
374
|
+
if (l.error) console.log(`! ${l.name}: ${l.error}`);
|
|
145
375
|
}
|
|
146
|
-
for (const
|
|
147
|
-
console.log(
|
|
376
|
+
for (const s of result.stripped) {
|
|
377
|
+
if (s.changed) console.log(`\u2713 removed old SmallDocs block from ${s.file}`);
|
|
378
|
+
if (s.error) console.log(`! ${s.file}: ${s.error}`);
|
|
379
|
+
if (s.reason === 'hand_edited') console.log(`! ${s.file}: local edits detected, left untouched`);
|
|
148
380
|
}
|
|
149
381
|
}
|
|
150
382
|
|
|
151
383
|
module.exports = {
|
|
152
|
-
|
|
153
|
-
fileHasBlock,
|
|
384
|
+
IS_WIN,
|
|
154
385
|
isSymlink,
|
|
155
386
|
atomicWrite,
|
|
156
387
|
backupFile,
|
|
157
388
|
acquireLock,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
389
|
+
copyDirRecursive,
|
|
390
|
+
refreshCanonicalSkill,
|
|
391
|
+
ensureSkillLink,
|
|
392
|
+
looksLikeOurSkillDir,
|
|
393
|
+
detectSkillAgents,
|
|
394
|
+
hasSetupEvidence,
|
|
395
|
+
stripLegacyBlocks,
|
|
396
|
+
syncAgentSkill,
|
|
397
|
+
syncChanged,
|
|
398
|
+
toImplicitResults,
|
|
399
|
+
printSyncSummary,
|
|
162
400
|
};
|
package/lib/cells-verify.js
CHANGED
|
@@ -86,6 +86,7 @@ function valuesFor(model, fxGrid) {
|
|
|
86
86
|
if (fx.kind === 'number') row.push(String(fx.value));
|
|
87
87
|
else if (fx.kind === 'error') row.push(fx.code);
|
|
88
88
|
else if (fx.kind === 'text') row.push(String(fx.value));
|
|
89
|
+
else if (fx.kind === 'boolean') row.push(fx.value ? 'TRUE' : 'FALSE');
|
|
89
90
|
else row.push('');
|
|
90
91
|
} else {
|
|
91
92
|
row.push(cell.raw);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const credentials = require('./cloud-credentials');
|
|
5
|
+
|
|
6
|
+
function bindingsFile() { return path.join(credentials.cloudDir(), 'bindings.json'); }
|
|
7
|
+
function pendingFile() { return path.join(credentials.cloudDir(), 'pending.json'); }
|
|
8
|
+
function basesDir() { return path.join(credentials.cloudDir(), 'bases'); }
|
|
9
|
+
|
|
10
|
+
function read(file) {
|
|
11
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return {}; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function canonical(file) {
|
|
15
|
+
const absolute = path.resolve(file);
|
|
16
|
+
try { return fs.realpathSync(absolute); } catch (_) { return absolute; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function key(accountId, file) { return accountId + '\0' + canonical(file); }
|
|
20
|
+
function get(accountId, file) { return read(bindingsFile())[key(accountId, file)] || null; }
|
|
21
|
+
|
|
22
|
+
function set(accountId, file, binding) {
|
|
23
|
+
const values = read(bindingsFile());
|
|
24
|
+
values[key(accountId, file)] = { ...binding, account_id: accountId, path: canonical(file) };
|
|
25
|
+
credentials.atomicWrite(bindingsFile(), values);
|
|
26
|
+
return values[key(accountId, file)];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getPending(accountId, file) { return read(pendingFile())[key(accountId, file)] || null; }
|
|
30
|
+
function setPending(accountId, file, value) {
|
|
31
|
+
const values = read(pendingFile());
|
|
32
|
+
values[key(accountId, file)] = value;
|
|
33
|
+
credentials.atomicWrite(pendingFile(), values);
|
|
34
|
+
}
|
|
35
|
+
function clearPending(accountId, file) {
|
|
36
|
+
const values = read(pendingFile());
|
|
37
|
+
delete values[key(accountId, file)];
|
|
38
|
+
credentials.atomicWrite(pendingFile(), values);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function operationKey(accountId, operation, resourceId) {
|
|
42
|
+
return accountId + '\0operation:' + operation + '\0' + resourceId;
|
|
43
|
+
}
|
|
44
|
+
function getOperationPending(accountId, operation, resourceId) {
|
|
45
|
+
return read(pendingFile())[operationKey(accountId, operation, resourceId)] || null;
|
|
46
|
+
}
|
|
47
|
+
function setOperationPending(accountId, operation, resourceId, value) {
|
|
48
|
+
const values = read(pendingFile());
|
|
49
|
+
values[operationKey(accountId, operation, resourceId)] = value;
|
|
50
|
+
credentials.atomicWrite(pendingFile(), values);
|
|
51
|
+
}
|
|
52
|
+
function clearOperationPending(accountId, operation, resourceId) {
|
|
53
|
+
const values = read(pendingFile());
|
|
54
|
+
delete values[operationKey(accountId, operation, resourceId)];
|
|
55
|
+
credentials.atomicWrite(pendingFile(), values);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hash(content) { return crypto.createHash('sha256').update(content).digest('hex'); }
|
|
59
|
+
|
|
60
|
+
function baseFile(accountId, documentId, revisionId) {
|
|
61
|
+
const root = path.resolve(basesDir());
|
|
62
|
+
const file = path.resolve(root, String(accountId), String(documentId), String(revisionId) + '.md');
|
|
63
|
+
if (file !== root && !file.startsWith(root + path.sep)) return null;
|
|
64
|
+
return file;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cacheBase(accountId, documentId, revisionId, content) {
|
|
68
|
+
const file = baseFile(accountId, documentId, revisionId);
|
|
69
|
+
if (!file) throw new Error('Cloud base path is invalid');
|
|
70
|
+
const dir = path.dirname(file);
|
|
71
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
72
|
+
fs.writeFileSync(file, content, { mode: 0o600 });
|
|
73
|
+
fs.chmodSync(file, 0o600);
|
|
74
|
+
return file;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readBase(accountId, documentId, revisionId) {
|
|
78
|
+
const file = baseFile(accountId, documentId, revisionId);
|
|
79
|
+
if (!file) return null;
|
|
80
|
+
try { return fs.readFileSync(file, 'utf8'); } catch (_) { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { bindingsFile, pendingFile, canonical, get, set, getPending, setPending,
|
|
84
|
+
clearPending, getOperationPending, setOperationPending, clearOperationPending, hash, cacheBase,
|
|
85
|
+
readBase };
|