@dinoxx/dinox-cli 1.0.18 → 1.0.20
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/README.md +807 -418
- package/dist/auth/userInfo.d.ts +2 -0
- package/dist/auth/userInfo.js +28 -11
- package/dist/cli.js +15 -1
- package/dist/commands/auth/index.d.ts +2 -0
- package/dist/commands/auth/index.js +217 -163
- package/dist/commands/boxes/index.d.ts +2 -0
- package/dist/commands/boxes/index.js +119 -75
- package/dist/commands/boxes/repo.d.ts +8 -0
- package/dist/commands/boxes/repo.js +41 -0
- package/dist/commands/config/index.d.ts +2 -0
- package/dist/commands/config/index.js +85 -36
- package/dist/commands/descriptor/register.d.ts +8 -0
- package/dist/commands/descriptor/register.js +113 -0
- package/dist/commands/descriptor/types.d.ts +37 -0
- package/dist/commands/descriptor/types.js +17 -0
- package/dist/commands/graph/index.d.ts +2 -0
- package/dist/commands/graph/index.js +143 -73
- package/dist/commands/info/index.d.ts +2 -0
- package/dist/commands/info/index.js +41 -14
- package/dist/commands/mcp/index.js +11 -10
- package/dist/commands/mcp/server.js +8 -4
- package/dist/commands/mcp/tools.js +30 -104
- package/dist/commands/mcp/types.d.ts +8 -1
- package/dist/commands/meta/index.d.ts +2 -0
- package/dist/commands/meta/index.js +64 -33
- package/dist/commands/meta/repo.d.ts +6 -0
- package/dist/commands/meta/repo.js +6 -0
- package/dist/commands/notes/actions.d.ts +10 -0
- package/dist/commands/notes/actions.js +403 -0
- package/dist/commands/notes/descriptors.d.ts +4 -0
- package/dist/commands/notes/descriptors.js +250 -0
- package/dist/commands/notes/dryRun.d.ts +31 -0
- package/dist/commands/notes/dryRun.js +130 -0
- package/dist/commands/notes/index.d.ts +2 -51
- package/dist/commands/notes/index.js +2 -581
- package/dist/commands/notes/repo.d.ts +9 -0
- package/dist/commands/notes/repo.js +56 -10
- package/dist/commands/notes/runtime.d.ts +28 -0
- package/dist/commands/notes/runtime.js +185 -0
- package/dist/commands/notes/searchOutput.d.ts +10 -2
- package/dist/commands/notes/searchOutput.js +14 -2
- package/dist/commands/notes/searchResponse.d.ts +51 -0
- package/dist/commands/notes/searchResponse.js +56 -0
- package/dist/commands/notes/searchSql.js +2 -0
- package/dist/commands/prompt/index.d.ts +2 -0
- package/dist/commands/prompt/index.js +122 -72
- package/dist/commands/prompt/repo.d.ts +7 -0
- package/dist/commands/prompt/repo.js +46 -0
- package/dist/commands/schema/commandMetadata.d.ts +2 -0
- package/dist/commands/schema/commandMetadata.js +18 -0
- package/dist/commands/schema/globalOptions.d.ts +3 -0
- package/dist/commands/schema/globalOptions.js +3 -0
- package/dist/commands/schema/index.d.ts +4 -0
- package/dist/commands/schema/index.js +109 -0
- package/dist/commands/schema/renderSkills.d.ts +6 -0
- package/dist/commands/schema/renderSkills.js +216 -0
- package/dist/commands/schema/spec.d.ts +6 -0
- package/dist/commands/schema/spec.js +40 -0
- package/dist/commands/schema/types.d.ts +47 -0
- package/dist/commands/schema/types.js +1 -0
- package/dist/commands/storage/index.d.ts +4 -0
- package/dist/commands/storage/index.js +292 -0
- package/dist/commands/storage/repo.d.ts +162 -0
- package/dist/commands/storage/repo.js +770 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +41 -27
- package/dist/commands/tags/index.d.ts +2 -0
- package/dist/commands/tags/index.js +121 -79
- package/dist/commands/tags/repo.d.ts +7 -0
- package/dist/commands/tags/repo.js +52 -0
- package/dist/commands/todo/dryRun.d.ts +23 -0
- package/dist/commands/todo/dryRun.js +133 -0
- package/dist/commands/todo/index.d.ts +2 -0
- package/dist/commands/todo/index.js +285 -135
- package/dist/commands/todo/repo.d.ts +11 -0
- package/dist/commands/todo/repo.js +37 -9
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.js +35 -13
- package/dist/commands/writeDryRun.d.ts +62 -0
- package/dist/commands/writeDryRun.js +131 -0
- package/dist/config/paths.d.ts +2 -1
- package/dist/config/paths.js +11 -1
- package/dist/config/resolve.js +7 -5
- package/dist/constants/links.d.ts +1 -1
- package/dist/constants/links.js +1 -1
- package/dist/daemon/ensure.js +3 -2
- package/dist/daemon/index.js +17 -15
- package/dist/dinox.js +9 -13
- package/dist/powersync/runtime.d.ts +1 -1
- package/dist/powersync/runtime.js +5 -3
- package/dist/powersync/schema/index.d.ts +27 -0
- package/dist/powersync/schema/index.js +2 -0
- package/dist/powersync/schema/note.d.ts +1 -0
- package/dist/powersync/schema/note.js +1 -0
- package/dist/powersync/schema/projects.d.ts +3 -0
- package/dist/powersync/schema/projects.js +3 -0
- package/dist/powersync/schema/storage.d.ts +24 -0
- package/dist/powersync/schema/storage.js +24 -0
- package/dist/skills/check.d.ts +6 -0
- package/dist/skills/check.js +254 -0
- package/dist/utils/bidlinkCanonicalization.d.ts +2 -0
- package/dist/utils/bidlinkCanonicalization.js +161 -0
- package/dist/utils/dryRun.d.ts +13 -0
- package/dist/utils/dryRun.js +69 -0
- package/dist/utils/output.d.ts +19 -0
- package/dist/utils/output.js +155 -1
- package/dist/utils/terminalSanitization.d.ts +2 -0
- package/dist/utils/terminalSanitization.js +33 -0
- package/dist/utils/tiptapMarkdown.d.ts +4 -1
- package/dist/utils/tiptapMarkdown.js +285 -113
- package/dist/utils/tiptapMarkdownSchema.d.ts +37 -0
- package/dist/utils/tiptapMarkdownSchema.js +577 -0
- package/dist/utils/tiptapMarkdownSchemaUtils.d.ts +74 -0
- package/dist/utils/tiptapMarkdownSchemaUtils.js +394 -0
- package/dist/utils/updateNotice.d.ts +7 -0
- package/dist/utils/updateNotice.js +64 -0
- package/package.json +25 -3
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { findCommandSchema, listCommandSchemas } from '../commands/schema/spec.js';
|
|
4
|
+
const EXEMPT_SHARED_REFERENCE = new Set(['dino-dinox', 'dino-shared']);
|
|
5
|
+
const SHARED_REFERENCE_PATTERN = /\.\.\/dino-shared\/SKILL\.md/;
|
|
6
|
+
const BINS_PATTERN = /^metadata:\s*[\s\S]*?^ requires:\s*[\s\S]*?^ bins:\s*\[(.*?)\]\s*$/m;
|
|
7
|
+
const CLI_HELP_PATTERN = /^metadata:\s*[\s\S]*?^ cliHelp:\s*.+$/m;
|
|
8
|
+
const commandSchemas = listCommandSchemas();
|
|
9
|
+
const commandSchemaPrefixes = commandSchemas
|
|
10
|
+
.map((schema) => ({
|
|
11
|
+
schema,
|
|
12
|
+
tokens: schema.command.split(/\s+/g),
|
|
13
|
+
}))
|
|
14
|
+
.sort((left, right) => right.tokens.length - left.tokens.length);
|
|
15
|
+
function parseFrontmatter(content) {
|
|
16
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
17
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
18
|
+
if (!match) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
frontmatter: match[1] ?? '',
|
|
23
|
+
body: match[2] ?? '',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function extractRelativeMarkdownLinks(body) {
|
|
27
|
+
const links = new Set();
|
|
28
|
+
const pattern = /\[[^\]]+\]\(([^)]+\.md)\)/g;
|
|
29
|
+
let match = null;
|
|
30
|
+
while ((match = pattern.exec(body)) !== null) {
|
|
31
|
+
const target = match[1]?.trim();
|
|
32
|
+
if (!target || target.startsWith('http://') || target.startsWith('https://')) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
links.add(target);
|
|
36
|
+
}
|
|
37
|
+
return [...links];
|
|
38
|
+
}
|
|
39
|
+
function walkMarkdownFiles(skillDir) {
|
|
40
|
+
const files = [];
|
|
41
|
+
for (const entry of fs.readdirSync(skillDir, { withFileTypes: true })) {
|
|
42
|
+
const fullPath = path.join(skillDir, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
files.push(...walkMarkdownFiles(fullPath));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
48
|
+
files.push(fullPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return files.sort();
|
|
52
|
+
}
|
|
53
|
+
function extractDinoCommands(markdown) {
|
|
54
|
+
const commands = [];
|
|
55
|
+
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
|
|
56
|
+
let inCodeFence = false;
|
|
57
|
+
let currentCommand = '';
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
const trimmed = line.trim();
|
|
60
|
+
if (trimmed.startsWith('```')) {
|
|
61
|
+
inCodeFence = !inCodeFence;
|
|
62
|
+
currentCommand = '';
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!inCodeFence || trimmed === '' || trimmed.startsWith('#')) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!currentCommand && !trimmed.startsWith('dino ')) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const continued = trimmed.endsWith('\\');
|
|
72
|
+
const segment = continued ? trimmed.slice(0, -1).trimEnd() : trimmed;
|
|
73
|
+
currentCommand = currentCommand ? `${currentCommand} ${segment}` : segment;
|
|
74
|
+
if (!continued) {
|
|
75
|
+
commands.push(currentCommand);
|
|
76
|
+
currentCommand = '';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return commands;
|
|
80
|
+
}
|
|
81
|
+
function tokenizeShellCommand(command) {
|
|
82
|
+
const tokens = [];
|
|
83
|
+
let current = '';
|
|
84
|
+
let quote = null;
|
|
85
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
86
|
+
const char = command[index];
|
|
87
|
+
if (quote) {
|
|
88
|
+
if (char === quote) {
|
|
89
|
+
quote = null;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
current += char;
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (char === '"' || char === "'") {
|
|
97
|
+
quote = char;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (/\s/.test(char)) {
|
|
101
|
+
if (current) {
|
|
102
|
+
tokens.push(current);
|
|
103
|
+
current = '';
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (char === '\\' && index + 1 < command.length) {
|
|
108
|
+
current += command[index + 1];
|
|
109
|
+
index += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
current += char;
|
|
113
|
+
}
|
|
114
|
+
if (current) {
|
|
115
|
+
tokens.push(current);
|
|
116
|
+
}
|
|
117
|
+
return tokens;
|
|
118
|
+
}
|
|
119
|
+
function extractOptionNames(tokens) {
|
|
120
|
+
return tokens
|
|
121
|
+
.filter((token) => token.startsWith('--'))
|
|
122
|
+
.map((token) => token.split('=', 1)[0] ?? token);
|
|
123
|
+
}
|
|
124
|
+
function normalizeOptionName(option) {
|
|
125
|
+
return option.trim().split(/\s+/g)[0] ?? option.trim();
|
|
126
|
+
}
|
|
127
|
+
function isPlaceholderToken(token) {
|
|
128
|
+
return /^[$<[].*/.test(token);
|
|
129
|
+
}
|
|
130
|
+
function validateDinoCommandExample(command) {
|
|
131
|
+
const tokens = tokenizeShellCommand(command);
|
|
132
|
+
if (tokens[0] !== 'dino') {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const match = commandSchemaPrefixes.find(({ tokens: prefix }) => prefix.every((token, index) => tokens[index] === token));
|
|
136
|
+
if (!match) {
|
|
137
|
+
return [`Unknown dino command example: ${command}`];
|
|
138
|
+
}
|
|
139
|
+
const allowedOptions = new Set([...(match.schema.options ?? []), ...match.schema.globalOptions.map((name) => ({ name }))]
|
|
140
|
+
.map((option) => normalizeOptionName(option.name)));
|
|
141
|
+
const optionIssues = extractOptionNames(tokens.slice(match.tokens.length))
|
|
142
|
+
.filter((option) => !allowedOptions.has(option))
|
|
143
|
+
.map((option) => `Unknown option ${option} in command example: ${command}`);
|
|
144
|
+
if (match.schema.path === 'schema') {
|
|
145
|
+
const schemaPathToken = tokens[match.tokens.length];
|
|
146
|
+
if (schemaPathToken && !schemaPathToken.startsWith('--') && !isPlaceholderToken(schemaPathToken)) {
|
|
147
|
+
if (!findCommandSchema(schemaPathToken)) {
|
|
148
|
+
optionIssues.push(`Unknown schema path ${schemaPathToken} in command example: ${command}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return optionIssues;
|
|
153
|
+
}
|
|
154
|
+
function checkMarkdownDocument(skill, content, relativePath, skillDir) {
|
|
155
|
+
const issues = [];
|
|
156
|
+
for (const relativeTarget of extractRelativeMarkdownLinks(content)) {
|
|
157
|
+
const resolved = path.resolve(path.dirname(path.join(skillDir, relativePath)), relativeTarget);
|
|
158
|
+
if (!fs.existsSync(resolved)) {
|
|
159
|
+
issues.push({ skill, message: `${relativePath}: referenced markdown file does not exist: ${relativeTarget}` });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const command of extractDinoCommands(content)) {
|
|
163
|
+
for (const issue of validateDinoCommandExample(command)) {
|
|
164
|
+
issues.push({ skill, message: `${relativePath}: ${issue}` });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return issues;
|
|
168
|
+
}
|
|
169
|
+
export function checkSkillContent(skill, content, skillDir) {
|
|
170
|
+
const issues = [];
|
|
171
|
+
const parsed = parseFrontmatter(content);
|
|
172
|
+
if (!parsed) {
|
|
173
|
+
return [{ skill, message: 'SKILL.md must start with valid YAML frontmatter.' }];
|
|
174
|
+
}
|
|
175
|
+
const { frontmatter, body } = parsed;
|
|
176
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
177
|
+
if (!nameMatch) {
|
|
178
|
+
issues.push({ skill, message: "Frontmatter is missing 'name'." });
|
|
179
|
+
}
|
|
180
|
+
else if (nameMatch[1]?.trim() !== skill) {
|
|
181
|
+
issues.push({ skill, message: `Frontmatter name must match directory name (${skill}).` });
|
|
182
|
+
}
|
|
183
|
+
if (!/^description:\s*/m.test(frontmatter)) {
|
|
184
|
+
issues.push({ skill, message: "Frontmatter is missing 'description'." });
|
|
185
|
+
}
|
|
186
|
+
if (!/^metadata:\s*$/m.test(frontmatter)) {
|
|
187
|
+
issues.push({ skill, message: "Frontmatter is missing 'metadata'." });
|
|
188
|
+
}
|
|
189
|
+
const binsMatch = frontmatter.match(BINS_PATTERN);
|
|
190
|
+
if (!binsMatch) {
|
|
191
|
+
issues.push({ skill, message: "Frontmatter must include metadata.requires.bins." });
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const bins = binsMatch[1]?.split(',').map((value) => value.replace(/["'\s]/g, '')).filter(Boolean) ?? [];
|
|
195
|
+
if (!bins.includes('dino')) {
|
|
196
|
+
issues.push({ skill, message: "metadata.requires.bins must include 'dino'." });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!CLI_HELP_PATTERN.test(frontmatter)) {
|
|
200
|
+
issues.push({ skill, message: "Frontmatter should include 'metadata.cliHelp'." });
|
|
201
|
+
}
|
|
202
|
+
if (skill === 'dino-shared' && !/^user-invocable:\s*false\s*$/m.test(frontmatter)) {
|
|
203
|
+
issues.push({ skill, message: "dino-shared must declare 'user-invocable: false'." });
|
|
204
|
+
}
|
|
205
|
+
if (skill === 'dino-dinox' && !/^user-invocable:\s*false\s*$/m.test(frontmatter)) {
|
|
206
|
+
issues.push({ skill, message: "dino-dinox must declare 'user-invocable: false'." });
|
|
207
|
+
}
|
|
208
|
+
if (!EXEMPT_SHARED_REFERENCE.has(skill) && !/^argument-hint:\s*/m.test(frontmatter)) {
|
|
209
|
+
issues.push({ skill, message: "User-invocable skills should declare 'argument-hint'." });
|
|
210
|
+
}
|
|
211
|
+
if (!EXEMPT_SHARED_REFERENCE.has(skill) && !/^allowed-tools:\s*$/m.test(frontmatter)) {
|
|
212
|
+
issues.push({ skill, message: "User-invocable skills should declare 'allowed-tools'." });
|
|
213
|
+
}
|
|
214
|
+
if (!EXEMPT_SHARED_REFERENCE.has(skill) && !SHARED_REFERENCE_PATTERN.test(body)) {
|
|
215
|
+
issues.push({ skill, message: 'User-invocable skills must tell the agent to read ../dino-shared/SKILL.md first.' });
|
|
216
|
+
}
|
|
217
|
+
if (!EXEMPT_SHARED_REFERENCE.has(skill) && !/^## Safety & Boundaries/m.test(body)) {
|
|
218
|
+
issues.push({ skill, message: "User-invocable skills should include a '## Safety & Boundaries' section." });
|
|
219
|
+
}
|
|
220
|
+
if (!EXEMPT_SHARED_REFERENCE.has(skill) && body.includes('dino ') && !body.includes('--format json')) {
|
|
221
|
+
issues.push({ skill, message: 'Skills that invoke dino commands must recommend --format json.' });
|
|
222
|
+
}
|
|
223
|
+
if (skill !== 'dino-dinox' && body.includes('--json')) {
|
|
224
|
+
issues.push({ skill, message: 'Skills should not recommend the legacy --json flag.' });
|
|
225
|
+
}
|
|
226
|
+
if (skillDir) {
|
|
227
|
+
issues.push(...checkMarkdownDocument(skill, body, 'SKILL.md', skillDir));
|
|
228
|
+
}
|
|
229
|
+
return issues;
|
|
230
|
+
}
|
|
231
|
+
export function checkSkillsDirectory(skillsDir) {
|
|
232
|
+
const issues = [];
|
|
233
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
234
|
+
.filter((entry) => entry.isDirectory())
|
|
235
|
+
.map((entry) => entry.name)
|
|
236
|
+
.sort();
|
|
237
|
+
for (const skill of entries) {
|
|
238
|
+
const skillFile = path.join(skillsDir, skill, 'SKILL.md');
|
|
239
|
+
if (!fs.existsSync(skillFile)) {
|
|
240
|
+
issues.push({ skill, message: 'Missing SKILL.md.' });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
244
|
+
issues.push(...checkSkillContent(skill, content, path.dirname(skillFile)));
|
|
245
|
+
const markdownFiles = walkMarkdownFiles(path.dirname(skillFile))
|
|
246
|
+
.filter((file) => file !== skillFile);
|
|
247
|
+
for (const markdownFile of markdownFiles) {
|
|
248
|
+
const relativePath = path.relative(path.dirname(skillFile), markdownFile);
|
|
249
|
+
const markdownContent = fs.readFileSync(markdownFile, 'utf8');
|
|
250
|
+
issues.push(...checkMarkdownDocument(skill, markdownContent, relativePath, path.dirname(skillFile)));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return issues;
|
|
254
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
function parseBidlinkRaw(raw) {
|
|
5
|
+
const match = raw.match(/^\[\[((?:\\.|[^\]\n])+?)\]\]$/);
|
|
6
|
+
if (!match) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const rawValue = match[1].trim();
|
|
10
|
+
if (!rawValue) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
let separatorIndex = -1;
|
|
14
|
+
for (let index = rawValue.length - 1; index >= 0; index -= 1) {
|
|
15
|
+
if (rawValue[index] === '|' && rawValue[index - 1] !== '\\') {
|
|
16
|
+
separatorIndex = index;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const unescape = (value) => value.replace(/\\([\\|\]])/g, '$1');
|
|
21
|
+
const label = unescape(separatorIndex >= 0 ? rawValue.slice(0, separatorIndex).trim() : rawValue.trim());
|
|
22
|
+
const explicitId = separatorIndex >= 0
|
|
23
|
+
? unescape(rawValue.slice(separatorIndex + 1).trim())
|
|
24
|
+
: null;
|
|
25
|
+
if (!label) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
explicitId: explicitId && explicitId.trim() ? explicitId : null,
|
|
30
|
+
hasExplicitId: separatorIndex >= 0 && Boolean(explicitId && explicitId.trim()),
|
|
31
|
+
label,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function escapeBidlinkValue(value) {
|
|
35
|
+
return value.replace(/([\\|\]])/g, '\\$1');
|
|
36
|
+
}
|
|
37
|
+
function collectBidlinkOccurrencesFromText(value, baseOffset) {
|
|
38
|
+
const occurrences = [];
|
|
39
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
40
|
+
if (value[index] !== '[' || value[index + 1] !== '[') {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const slice = value.slice(index);
|
|
44
|
+
const match = slice.match(/^\[\[((?:\\.|[^\]\n])+?)\]\]/);
|
|
45
|
+
if (!match) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const raw = match[0];
|
|
49
|
+
const parsed = parseBidlinkRaw(raw);
|
|
50
|
+
if (!parsed || parsed.hasExplicitId) {
|
|
51
|
+
index += raw.length - 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
occurrences.push({
|
|
55
|
+
endOffset: baseOffset + index + raw.length,
|
|
56
|
+
label: parsed.label,
|
|
57
|
+
raw,
|
|
58
|
+
startOffset: baseOffset + index,
|
|
59
|
+
});
|
|
60
|
+
index += raw.length - 1;
|
|
61
|
+
}
|
|
62
|
+
return occurrences;
|
|
63
|
+
}
|
|
64
|
+
function collectBidlinkOccurrences(markdown) {
|
|
65
|
+
const tree = unified().use(remarkParse).use(remarkGfm).parse(markdown);
|
|
66
|
+
const stack = [tree];
|
|
67
|
+
const occurrences = [];
|
|
68
|
+
while (stack.length > 0) {
|
|
69
|
+
const node = stack.pop();
|
|
70
|
+
if (!node) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (node.type === 'text' && typeof node.value === 'string') {
|
|
74
|
+
const startOffset = node.position?.start?.offset;
|
|
75
|
+
const endOffset = node.position?.end?.offset;
|
|
76
|
+
if (typeof startOffset === 'number' &&
|
|
77
|
+
typeof endOffset === 'number' &&
|
|
78
|
+
endOffset >= startOffset) {
|
|
79
|
+
occurrences.push(...collectBidlinkOccurrencesFromText(node.value, startOffset));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const children = Array.isArray(node.children) ? node.children : [];
|
|
83
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
84
|
+
stack.push(children[index]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return occurrences;
|
|
88
|
+
}
|
|
89
|
+
async function resolveLatestNoteIdsByTitles(db, userId, titles) {
|
|
90
|
+
const normalizedTitles = Array.from(new Set(titles
|
|
91
|
+
.map((title) => title.trim())
|
|
92
|
+
.filter((title) => title.length > 0)));
|
|
93
|
+
if (normalizedTitles.length === 0) {
|
|
94
|
+
return new Map();
|
|
95
|
+
}
|
|
96
|
+
const columns = await db.getAll('PRAGMA table_info(c_note)', []);
|
|
97
|
+
const columnNames = new Set(columns
|
|
98
|
+
.map((row) => row.name?.trim().toLowerCase() ?? '')
|
|
99
|
+
.filter((name) => name.length > 0));
|
|
100
|
+
const userColumns = [];
|
|
101
|
+
if (columnNames.has('user_id')) {
|
|
102
|
+
userColumns.push('user_id');
|
|
103
|
+
}
|
|
104
|
+
if (columnNames.has('user_no')) {
|
|
105
|
+
userColumns.push('user_no');
|
|
106
|
+
}
|
|
107
|
+
if (!columnNames.has('title') || !columnNames.has('id') || userColumns.length === 0) {
|
|
108
|
+
return new Map();
|
|
109
|
+
}
|
|
110
|
+
const userClauses = userColumns.map((column) => `n.${column} = ?`);
|
|
111
|
+
const titlePlaceholders = normalizedTitles.map(() => '?').join(', ');
|
|
112
|
+
const params = [...userColumns.map(() => userId), ...normalizedTitles];
|
|
113
|
+
const rows = await db.getAll(`
|
|
114
|
+
SELECT
|
|
115
|
+
n.id,
|
|
116
|
+
TRIM(n.title) AS title_key
|
|
117
|
+
FROM c_note n
|
|
118
|
+
WHERE (${userClauses.join(' OR ')})
|
|
119
|
+
AND (n.is_del IS NULL OR n.is_del = 0)
|
|
120
|
+
AND TRIM(n.title) IN (${titlePlaceholders})
|
|
121
|
+
ORDER BY title_key ASC, COALESCE(n.created_at, '') DESC, n.id DESC
|
|
122
|
+
`, params);
|
|
123
|
+
const resolved = new Map();
|
|
124
|
+
for (const row of rows) {
|
|
125
|
+
const titleKey = typeof row.title_key === 'string' ? row.title_key.trim() : '';
|
|
126
|
+
const id = typeof row.id === 'string' ? row.id.trim() : '';
|
|
127
|
+
if (!titleKey || !id || resolved.has(titleKey)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
resolved.set(titleKey, id);
|
|
131
|
+
}
|
|
132
|
+
return resolved;
|
|
133
|
+
}
|
|
134
|
+
export async function canonicalizeBidlinksInMarkdown(db, markdown, userId) {
|
|
135
|
+
const source = typeof markdown === 'string' ? markdown : String(markdown ?? '');
|
|
136
|
+
if (!source.includes('[[') || !userId.trim()) {
|
|
137
|
+
return source;
|
|
138
|
+
}
|
|
139
|
+
const occurrences = collectBidlinkOccurrences(source);
|
|
140
|
+
if (occurrences.length === 0) {
|
|
141
|
+
return source;
|
|
142
|
+
}
|
|
143
|
+
const resolvedIdsByTitle = await resolveLatestNoteIdsByTitles(db, userId, occurrences.map((occurrence) => occurrence.label));
|
|
144
|
+
if (resolvedIdsByTitle.size === 0) {
|
|
145
|
+
return source;
|
|
146
|
+
}
|
|
147
|
+
let nextMarkdown = source;
|
|
148
|
+
const sorted = [...occurrences].sort((left, right) => right.startOffset - left.startOffset);
|
|
149
|
+
for (const occurrence of sorted) {
|
|
150
|
+
const resolvedId = resolvedIdsByTitle.get(occurrence.label);
|
|
151
|
+
if (!resolvedId) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const replacement = `[[${escapeBidlinkValue(occurrence.label)}|${escapeBidlinkValue(resolvedId)}]]`;
|
|
155
|
+
nextMarkdown =
|
|
156
|
+
nextMarkdown.slice(0, occurrence.startOffset) +
|
|
157
|
+
replacement +
|
|
158
|
+
nextMarkdown.slice(occurrence.endOffset);
|
|
159
|
+
}
|
|
160
|
+
return nextMarkdown;
|
|
161
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type DryRunPreview = {
|
|
2
|
+
dry_run: true;
|
|
3
|
+
command: string;
|
|
4
|
+
action: string;
|
|
5
|
+
risk: 'write' | 'high-risk-write';
|
|
6
|
+
summary: string;
|
|
7
|
+
warnings?: string[];
|
|
8
|
+
inputs?: Record<string, unknown>;
|
|
9
|
+
targets?: Array<Record<string, unknown>>;
|
|
10
|
+
changes?: Record<string, unknown> | Array<Record<string, unknown>>;
|
|
11
|
+
};
|
|
12
|
+
export declare function formatDryRunPreview(preview: DryRunPreview): string;
|
|
13
|
+
export declare function printDryRunPreview(preview: DryRunPreview): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableBlock, printSuccess, } from './output.js';
|
|
2
|
+
function formatValue(value) {
|
|
3
|
+
if (value === null || value === undefined) {
|
|
4
|
+
return String(value);
|
|
5
|
+
}
|
|
6
|
+
if (typeof value === 'string') {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
return JSON.stringify(value);
|
|
10
|
+
}
|
|
11
|
+
function formatRecord(record, prefix = ' ') {
|
|
12
|
+
return Object.entries(record).map(([key, value]) => `${prefix}${key}: ${formatValue(value)}`);
|
|
13
|
+
}
|
|
14
|
+
function formatTargets(targets) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
targets.forEach((target, index) => {
|
|
17
|
+
lines.push(` [${index + 1}]`);
|
|
18
|
+
lines.push(...formatRecord(target, ' '));
|
|
19
|
+
});
|
|
20
|
+
return lines;
|
|
21
|
+
}
|
|
22
|
+
function formatChanges(value) {
|
|
23
|
+
if (!value) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return formatTargets(value);
|
|
28
|
+
}
|
|
29
|
+
return formatRecord(value);
|
|
30
|
+
}
|
|
31
|
+
export function formatDryRunPreview(preview) {
|
|
32
|
+
const lines = [
|
|
33
|
+
'=== Dry Run ===',
|
|
34
|
+
`Action: ${preview.action}`,
|
|
35
|
+
`Command: ${preview.command}`,
|
|
36
|
+
`Risk: ${preview.risk}`,
|
|
37
|
+
`Summary: ${preview.summary}`,
|
|
38
|
+
];
|
|
39
|
+
if (preview.warnings && preview.warnings.length > 0) {
|
|
40
|
+
lines.push('');
|
|
41
|
+
lines.push('Warnings:');
|
|
42
|
+
for (const warning of preview.warnings) {
|
|
43
|
+
lines.push(` - ${warning}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (preview.inputs && Object.keys(preview.inputs).length > 0) {
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push('Inputs:');
|
|
49
|
+
lines.push(...formatRecord(preview.inputs));
|
|
50
|
+
}
|
|
51
|
+
if (preview.targets && preview.targets.length > 0) {
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push('Targets:');
|
|
54
|
+
lines.push(...formatTargets(preview.targets));
|
|
55
|
+
}
|
|
56
|
+
if (preview.changes && (Array.isArray(preview.changes) ? preview.changes.length > 0 : Object.keys(preview.changes).length > 0)) {
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push('Changes:');
|
|
59
|
+
lines.push(...formatChanges(preview.changes));
|
|
60
|
+
}
|
|
61
|
+
return `${lines.join('\n')}\n`;
|
|
62
|
+
}
|
|
63
|
+
export function printDryRunPreview(preview) {
|
|
64
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
65
|
+
printSuccess(preview);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
printHumanReadableBlock(formatDryRunPreview(preview));
|
|
69
|
+
}
|
package/dist/utils/output.d.ts
CHANGED
|
@@ -1,2 +1,21 @@
|
|
|
1
|
+
export type StructuredOutputFormat = 'yaml' | 'json';
|
|
2
|
+
export type StructuredNoticeMap = Record<string, unknown>;
|
|
1
3
|
export declare function toYaml(value: unknown): string;
|
|
4
|
+
export declare function toJson(value: unknown): string;
|
|
5
|
+
export declare function printHumanReadableLine(text: string): void;
|
|
6
|
+
export declare function printHumanReadableBlock(text: string): void;
|
|
7
|
+
export declare function printHumanReadableError(text: string): void;
|
|
8
|
+
export declare function resolveStructuredOutputFormat(input: {
|
|
9
|
+
format?: unknown;
|
|
10
|
+
json?: unknown;
|
|
11
|
+
}): StructuredOutputFormat | null;
|
|
12
|
+
export declare function parseStructuredOutputFormatFromArgv(argv: string[]): StructuredOutputFormat | null;
|
|
13
|
+
export declare function setRequestedStructuredOutputFormat(format: StructuredOutputFormat | null): void;
|
|
14
|
+
export declare function clearRequestedStructuredOutputFormat(): void;
|
|
15
|
+
export declare function getRequestedStructuredOutputFormat(): StructuredOutputFormat | null;
|
|
16
|
+
export declare function setPendingStructuredNoticeProvider(provider: (() => StructuredNoticeMap | null) | null): void;
|
|
17
|
+
export declare function clearPendingStructuredNoticeProvider(): void;
|
|
18
|
+
export declare function printStructuredData(value: unknown, format?: StructuredOutputFormat | null): void;
|
|
19
|
+
export declare function printSuccess(value: unknown, format?: StructuredOutputFormat | null): void;
|
|
20
|
+
export declare function printError(value: unknown, format?: StructuredOutputFormat | null): void;
|
|
2
21
|
export declare function printYaml(value: unknown): void;
|
package/dist/utils/output.js
CHANGED
|
@@ -1,10 +1,164 @@
|
|
|
1
1
|
import { stringify as stringifyYaml } from 'yaml';
|
|
2
|
+
import { sanitizeForTerminal } from './terminalSanitization.js';
|
|
3
|
+
let requestedStructuredOutputFormat = null;
|
|
4
|
+
let pendingStructuredNoticeProvider = null;
|
|
2
5
|
export function toYaml(value) {
|
|
3
6
|
return stringifyYaml(value, {
|
|
4
7
|
lineWidth: 0,
|
|
5
8
|
indent: 2,
|
|
6
9
|
});
|
|
7
10
|
}
|
|
11
|
+
export function toJson(value) {
|
|
12
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
13
|
+
}
|
|
14
|
+
function writeToStdout(text) {
|
|
15
|
+
process.stdout.write(text);
|
|
16
|
+
}
|
|
17
|
+
function writeToStderr(text) {
|
|
18
|
+
process.stderr.write(text);
|
|
19
|
+
}
|
|
20
|
+
export function printHumanReadableLine(text) {
|
|
21
|
+
writeToStdout(`${sanitizeForTerminal(text)}\n`);
|
|
22
|
+
}
|
|
23
|
+
export function printHumanReadableBlock(text) {
|
|
24
|
+
writeToStdout(sanitizeForTerminal(text));
|
|
25
|
+
}
|
|
26
|
+
export function printHumanReadableError(text) {
|
|
27
|
+
writeToStderr(`${sanitizeForTerminal(text)}\n`);
|
|
28
|
+
}
|
|
29
|
+
function normalizeStructuredOutputFormat(value) {
|
|
30
|
+
if (typeof value !== 'string') {
|
|
31
|
+
throw new Error('--format must be one of: yaml, json');
|
|
32
|
+
}
|
|
33
|
+
const normalized = value.trim().toLowerCase();
|
|
34
|
+
if (normalized === 'yaml' || normalized === 'json') {
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
throw new Error('--format must be one of: yaml, json');
|
|
38
|
+
}
|
|
39
|
+
export function resolveStructuredOutputFormat(input) {
|
|
40
|
+
if (input.format !== undefined && input.format !== null && input.format !== '') {
|
|
41
|
+
return normalizeStructuredOutputFormat(input.format);
|
|
42
|
+
}
|
|
43
|
+
if (Boolean(input.json)) {
|
|
44
|
+
return 'yaml';
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
export function parseStructuredOutputFormatFromArgv(argv) {
|
|
49
|
+
let format = null;
|
|
50
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
51
|
+
const arg = argv[index];
|
|
52
|
+
if (!arg) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg === '--json') {
|
|
56
|
+
format = 'yaml';
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg === '--format') {
|
|
60
|
+
format = normalizeStructuredOutputFormat(argv[index + 1]);
|
|
61
|
+
index += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (arg.startsWith('--format=')) {
|
|
65
|
+
format = normalizeStructuredOutputFormat(arg.slice('--format='.length));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return format;
|
|
69
|
+
}
|
|
70
|
+
export function setRequestedStructuredOutputFormat(format) {
|
|
71
|
+
requestedStructuredOutputFormat = format;
|
|
72
|
+
}
|
|
73
|
+
export function clearRequestedStructuredOutputFormat() {
|
|
74
|
+
requestedStructuredOutputFormat = null;
|
|
75
|
+
}
|
|
76
|
+
export function getRequestedStructuredOutputFormat() {
|
|
77
|
+
return requestedStructuredOutputFormat;
|
|
78
|
+
}
|
|
79
|
+
export function setPendingStructuredNoticeProvider(provider) {
|
|
80
|
+
pendingStructuredNoticeProvider = provider;
|
|
81
|
+
}
|
|
82
|
+
export function clearPendingStructuredNoticeProvider() {
|
|
83
|
+
pendingStructuredNoticeProvider = null;
|
|
84
|
+
}
|
|
85
|
+
function getPendingStructuredNotice() {
|
|
86
|
+
if (!pendingStructuredNoticeProvider) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return pendingStructuredNoticeProvider();
|
|
90
|
+
}
|
|
91
|
+
export function printStructuredData(value, format = getRequestedStructuredOutputFormat()) {
|
|
92
|
+
if (format === 'json') {
|
|
93
|
+
writeToStdout(toJson(value));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
writeToStdout(toYaml(value));
|
|
97
|
+
}
|
|
98
|
+
function normalizeSuccessEnvelopeData(value) {
|
|
99
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
const record = value;
|
|
103
|
+
if (record.ok !== true) {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
const { ok: _ignored, ...rest } = record;
|
|
107
|
+
return rest;
|
|
108
|
+
}
|
|
109
|
+
function deriveStructuredNotice(value) {
|
|
110
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const record = value;
|
|
114
|
+
const notice = {};
|
|
115
|
+
if (record.stale === true) {
|
|
116
|
+
notice.stale_cache = {
|
|
117
|
+
message: 'Result may be stale because the local cache did not fully synchronize.',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (record.uploadEnabled === false) {
|
|
121
|
+
notice.upload_disabled = {
|
|
122
|
+
message: 'Local changes cannot upload because powersync.uploadBaseUrl is unset.',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return Object.keys(notice).length > 0 ? notice : null;
|
|
126
|
+
}
|
|
127
|
+
function mergeStructuredNotices(base, derived) {
|
|
128
|
+
const merged = {
|
|
129
|
+
...(base ?? {}),
|
|
130
|
+
...(derived ?? {}),
|
|
131
|
+
};
|
|
132
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
133
|
+
}
|
|
134
|
+
export function printSuccess(value, format = getRequestedStructuredOutputFormat()) {
|
|
135
|
+
if (format === 'json') {
|
|
136
|
+
const data = normalizeSuccessEnvelopeData(value);
|
|
137
|
+
const notice = mergeStructuredNotices(getPendingStructuredNotice(), deriveStructuredNotice(data));
|
|
138
|
+
printStructuredData({
|
|
139
|
+
ok: true,
|
|
140
|
+
data,
|
|
141
|
+
...(notice ? { _notice: notice } : {}),
|
|
142
|
+
}, 'json');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
printStructuredData(value, format);
|
|
146
|
+
}
|
|
147
|
+
export function printError(value, format = getRequestedStructuredOutputFormat()) {
|
|
148
|
+
if (format === 'json') {
|
|
149
|
+
const notice = getPendingStructuredNotice();
|
|
150
|
+
writeToStderr(toJson({
|
|
151
|
+
ok: false,
|
|
152
|
+
error: value,
|
|
153
|
+
...(notice ? { _notice: notice } : {}),
|
|
154
|
+
}));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
writeToStdout(toYaml({
|
|
158
|
+
ok: false,
|
|
159
|
+
error: value,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
8
162
|
export function printYaml(value) {
|
|
9
|
-
|
|
163
|
+
printStructuredData(value);
|
|
10
164
|
}
|