@dinoxx/dinox-cli 1.1.1 → 2.1.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/README.md +44 -3
- package/dist/cli.js +4 -0
- package/dist/commands/auth/index.js +1 -2
- package/dist/commands/boxes/index.js +37 -13
- package/dist/commands/boxes/operations.d.ts +3 -3
- package/dist/commands/boxes/operations.js +51 -28
- package/dist/commands/info/index.js +30 -5
- package/dist/commands/meta/repo.js +2 -1
- package/dist/commands/notes/actions.js +20 -37
- package/dist/commands/notes/bulk.d.ts +2 -2
- package/dist/commands/notes/bulk.js +3 -18
- package/dist/commands/notes/contentPatch.d.ts +5 -1
- package/dist/commands/notes/contentPatch.js +12 -1
- package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
- package/dist/commands/notes/contentPatchCommand.js +29 -8
- package/dist/commands/notes/descriptors.js +19 -16
- package/dist/commands/notes/export.js +3 -0
- package/dist/commands/notes/repo/read.d.ts +8 -0
- package/dist/commands/notes/repo/read.js +321 -0
- package/dist/commands/notes/repo/resolve.d.ts +8 -0
- package/dist/commands/notes/repo/resolve.js +187 -0
- package/dist/commands/notes/repo/search.d.ts +42 -0
- package/dist/commands/notes/repo/search.js +513 -0
- package/dist/commands/notes/repo/shared.d.ts +190 -0
- package/dist/commands/notes/repo/shared.js +102 -0
- package/dist/commands/notes/repo/write.d.ts +24 -0
- package/dist/commands/notes/repo/write.js +251 -0
- package/dist/commands/notes/repo.d.ts +7 -177
- package/dist/commands/notes/repo.js +7 -1310
- package/dist/commands/notes/searchOutput.d.ts +2 -1
- package/dist/commands/notes/searchOutput.js +2 -1
- package/dist/commands/notes/searchSql.js +9 -3
- package/dist/commands/prompt/index.d.ts +1 -0
- package/dist/commands/prompt/index.js +26 -11
- package/dist/commands/savedViews/index.d.ts +7 -0
- package/dist/commands/savedViews/index.js +458 -0
- package/dist/commands/savedViews/model.d.ts +87 -0
- package/dist/commands/savedViews/model.js +306 -0
- package/dist/commands/savedViews/repo.d.ts +123 -0
- package/dist/commands/savedViews/repo.js +512 -0
- package/dist/commands/savedViews/semantics.d.ts +46 -0
- package/dist/commands/savedViews/semantics.js +738 -0
- package/dist/commands/schema/commandMetadata.js +4 -4
- package/dist/commands/schema/index.d.ts +1 -0
- package/dist/commands/schema/index.js +23 -2
- package/dist/commands/schema/renderSkills.d.ts +6 -0
- package/dist/commands/schema/renderSkills.js +109 -11
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/skills/index.d.ts +80 -0
- package/dist/commands/skills/index.js +572 -0
- package/dist/commands/storage/index.js +2 -2
- package/dist/commands/storage/repo/configs.d.ts +30 -0
- package/dist/commands/storage/repo/configs.js +259 -0
- package/dist/commands/storage/repo/operations.d.ts +25 -0
- package/dist/commands/storage/repo/operations.js +337 -0
- package/dist/commands/storage/repo/remote.d.ts +32 -0
- package/dist/commands/storage/repo/remote.js +132 -0
- package/dist/commands/storage/repo/resources.d.ts +12 -0
- package/dist/commands/storage/repo/resources.js +134 -0
- package/dist/commands/storage/repo/shared.d.ts +181 -0
- package/dist/commands/storage/repo/shared.js +36 -0
- package/dist/commands/storage/repo/targets.d.ts +12 -0
- package/dist/commands/storage/repo/targets.js +178 -0
- package/dist/commands/storage/repo.d.ts +3 -166
- package/dist/commands/storage/repo.js +4 -1034
- package/dist/commands/sync.js +2 -1
- package/dist/commands/tags/index.js +30 -11
- package/dist/commands/tags/operations.d.ts +3 -3
- package/dist/commands/tags/operations.js +45 -20
- package/dist/commands/templates/model.d.ts +50 -0
- package/dist/commands/templates/model.js +239 -0
- package/dist/commands/todo/index.js +3 -3
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +46 -9
- package/dist/commands/writeConfirmation.d.ts +13 -0
- package/dist/commands/writeConfirmation.js +54 -0
- package/dist/config/resolve.js +5 -10
- package/dist/config/types.d.ts +3 -6
- package/dist/constants/links.d.ts +18 -1
- package/dist/constants/links.js +26 -1
- package/dist/contract/snapshot.d.ts +30 -0
- package/dist/contract/snapshot.js +114 -0
- package/dist/daemon/client.d.ts +2 -2
- package/dist/daemon/client.js +108 -54
- package/dist/daemon/index.d.ts +5 -3
- package/dist/daemon/index.js +28 -5
- package/dist/daemon/lifecycle.d.ts +7 -2
- package/dist/daemon/lifecycle.js +53 -13
- package/dist/daemon/pid.d.ts +1 -1
- package/dist/daemon/pid.js +6 -3
- package/dist/dinox.js +9 -1
- package/dist/powersync/exclusiveRuntime.d.ts +1 -0
- package/dist/powersync/exclusiveRuntime.js +26 -8
- package/dist/powersync/runtime.d.ts +3 -2
- package/dist/powersync/runtime.js +19 -3
- package/dist/powersync/schema/content.d.ts +1 -78
- package/dist/powersync/schema/content.js +1 -78
- package/dist/powersync/schema/electronCanonical.d.ts +1561 -0
- package/dist/powersync/schema/electronCanonical.js +1154 -0
- package/dist/powersync/schema/index.d.ts +573 -129
- package/dist/powersync/schema/index.js +3 -32
- package/dist/powersync/schema/local.d.ts +1 -56
- package/dist/powersync/schema/local.js +1 -60
- package/dist/powersync/schema/note.d.ts +1 -35
- package/dist/powersync/schema/note.js +1 -35
- package/dist/powersync/schema/notesExtras.d.ts +1 -62
- package/dist/powersync/schema/notesExtras.js +1 -71
- package/dist/powersync/schema/projects.d.ts +1 -104
- package/dist/powersync/schema/projects.js +1 -104
- package/dist/powersync/schema/savedViews.d.ts +1 -0
- package/dist/powersync/schema/savedViews.js +1 -0
- package/dist/powersync/schema/storage.d.ts +1 -24
- package/dist/powersync/schema/storage.js +1 -24
- package/dist/powersync/schema/tags.d.ts +1 -37
- package/dist/powersync/schema/tags.js +1 -37
- package/dist/powersync/syncGate.d.ts +20 -2
- package/dist/powersync/uploader.d.ts +16 -2
- package/dist/powersync/uploader.js +152 -158
- package/dist/skills/check.d.ts +1 -0
- package/dist/skills/check.js +538 -70
- package/dist/skills/commandRefs.d.ts +21 -0
- package/dist/skills/commandRefs.js +132 -0
- package/dist/skills/generate.d.ts +13 -0
- package/dist/skills/generate.js +80 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +58 -0
- package/dist/skills/retired.d.ts +60 -0
- package/dist/skills/retired.js +58 -0
- package/dist/utils/deadline.d.ts +8 -0
- package/dist/utils/deadline.js +31 -0
- package/dist/utils/dryRun.d.ts +20 -1
- package/dist/utils/dryRun.js +22 -3
- package/dist/utils/errorCodes.d.ts +311 -0
- package/dist/utils/errorCodes.js +92 -0
- package/dist/utils/errors.d.ts +15 -2
- package/dist/utils/errors.js +13 -0
- package/dist/utils/processExit.d.ts +1 -0
- package/dist/utils/processExit.js +33 -0
- package/dist/utils/updateNotice.d.ts +1 -0
- package/dist/utils/updateNotice.js +1 -1
- package/package.json +18 -8
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parse as parseYaml } from 'yaml';
|
|
6
|
+
import { getSkillsReleaseMetadata, SKILLS_INSTALL_AGENTS, SKILLS_INSTALLER_NAME, SKILLS_INSTALLER_VERSION, SKILLS_REPO_SLUG, } from '../../constants/links.js';
|
|
7
|
+
import { listRegisteredSkillNames } from '../../skills/registry.js';
|
|
8
|
+
import { RETIRED_SKILLS } from '../../skills/retired.js';
|
|
9
|
+
import { getPackageVersion } from '../../utils/version.js';
|
|
10
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
11
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
12
|
+
import { registerCommandGroup } from '../descriptor/register.js';
|
|
13
|
+
import { FORMAT_OPTIONS } from '../schema/globalOptions.js';
|
|
14
|
+
const MAX_CAPTURED_INSTALLER_OUTPUT_BYTES = 64 * 1024;
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function defaultLockFilePath() {
|
|
19
|
+
return path.join(homedir(), '.agents', '.skill-lock.json');
|
|
20
|
+
}
|
|
21
|
+
async function defaultReadLockFile(lockFilePath) {
|
|
22
|
+
return readFile(lockFilePath, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
function absentScan(lockFilePath) {
|
|
25
|
+
const skills = RETIRED_SKILLS.map((skill) => ({
|
|
26
|
+
name: skill.name,
|
|
27
|
+
replacement: skill.replacement,
|
|
28
|
+
...('replacementNote' in skill ? { replacementNote: skill.replacementNote } : {}),
|
|
29
|
+
status: 'absent',
|
|
30
|
+
expected: {
|
|
31
|
+
source: skill.source,
|
|
32
|
+
skillFolderHash: skill.skillFolderHash,
|
|
33
|
+
},
|
|
34
|
+
observed: null,
|
|
35
|
+
mismatches: [],
|
|
36
|
+
}));
|
|
37
|
+
return {
|
|
38
|
+
lockFile: lockFilePath,
|
|
39
|
+
lockFound: false,
|
|
40
|
+
lockVersion: null,
|
|
41
|
+
summary: {
|
|
42
|
+
total: skills.length,
|
|
43
|
+
absent: skills.length,
|
|
44
|
+
safeToRemove: 0,
|
|
45
|
+
conflict: 0,
|
|
46
|
+
},
|
|
47
|
+
skills,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function invalidLockError(lockFilePath, message, cause) {
|
|
51
|
+
return new DinoxError(`Invalid skills installer lock at ${lockFilePath}: ${message}`, {
|
|
52
|
+
code: 'SKILLS_LOCK_INVALID',
|
|
53
|
+
cause,
|
|
54
|
+
details: { lockFile: lockFilePath },
|
|
55
|
+
suggestion: 'Inspect the lock file without deleting it, then rerun `dino skills doctor --format json`.',
|
|
56
|
+
recoverable: true,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function readSkillsLock(lockFilePath, readLockFile) {
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = await readLockFile(lockFilePath);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error?.code === 'ENOENT') {
|
|
66
|
+
return { found: false, version: null, entries: {} };
|
|
67
|
+
}
|
|
68
|
+
throw new DinoxError(`Failed to read skills installer lock at ${lockFilePath}`, {
|
|
69
|
+
code: 'SKILLS_LOCK_READ_FAILED',
|
|
70
|
+
cause: error,
|
|
71
|
+
details: { lockFile: lockFilePath },
|
|
72
|
+
suggestion: 'Check file permissions, then rerun `dino skills doctor --format json`.',
|
|
73
|
+
recoverable: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw invalidLockError(lockFilePath, 'file is not valid JSON', error);
|
|
82
|
+
}
|
|
83
|
+
if (!isRecord(parsed) || !isRecord(parsed.skills)) {
|
|
84
|
+
throw invalidLockError(lockFilePath, 'expected an object with a skills map');
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
found: true,
|
|
88
|
+
version: typeof parsed.version === 'number' ? parsed.version : null,
|
|
89
|
+
entries: parsed.skills,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export async function scanRetiredSkillsLock(lockFilePath = defaultLockFilePath(), readLockFile = defaultReadLockFile) {
|
|
93
|
+
const lock = await readSkillsLock(lockFilePath, readLockFile);
|
|
94
|
+
if (!lock.found) {
|
|
95
|
+
return absentScan(lockFilePath);
|
|
96
|
+
}
|
|
97
|
+
return scanRetiredSkills(lockFilePath, lock);
|
|
98
|
+
}
|
|
99
|
+
function scanRetiredSkills(lockFilePath, lock) {
|
|
100
|
+
const entries = lock.entries;
|
|
101
|
+
const skills = RETIRED_SKILLS.map((skill) => {
|
|
102
|
+
if (!Object.prototype.hasOwnProperty.call(entries, skill.name)) {
|
|
103
|
+
return {
|
|
104
|
+
name: skill.name,
|
|
105
|
+
replacement: skill.replacement,
|
|
106
|
+
...('replacementNote' in skill ? { replacementNote: skill.replacementNote } : {}),
|
|
107
|
+
status: 'absent',
|
|
108
|
+
expected: {
|
|
109
|
+
source: skill.source,
|
|
110
|
+
skillFolderHash: skill.skillFolderHash,
|
|
111
|
+
},
|
|
112
|
+
observed: null,
|
|
113
|
+
mismatches: [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const rawEntry = entries[skill.name];
|
|
117
|
+
const entry = isRecord(rawEntry) ? rawEntry : {};
|
|
118
|
+
const observed = {
|
|
119
|
+
source: typeof entry.source === 'string' ? entry.source : null,
|
|
120
|
+
skillFolderHash: typeof entry.skillFolderHash === 'string' ? entry.skillFolderHash : null,
|
|
121
|
+
};
|
|
122
|
+
const mismatches = [];
|
|
123
|
+
if (observed.source !== skill.source)
|
|
124
|
+
mismatches.push('source');
|
|
125
|
+
if (observed.skillFolderHash !== skill.skillFolderHash)
|
|
126
|
+
mismatches.push('skillFolderHash');
|
|
127
|
+
return {
|
|
128
|
+
name: skill.name,
|
|
129
|
+
replacement: skill.replacement,
|
|
130
|
+
...('replacementNote' in skill ? { replacementNote: skill.replacementNote } : {}),
|
|
131
|
+
status: mismatches.length === 0 ? 'safe-to-remove' : 'conflict',
|
|
132
|
+
expected: {
|
|
133
|
+
source: skill.source,
|
|
134
|
+
skillFolderHash: skill.skillFolderHash,
|
|
135
|
+
},
|
|
136
|
+
observed,
|
|
137
|
+
mismatches,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
lockFile: lockFilePath,
|
|
142
|
+
lockFound: true,
|
|
143
|
+
lockVersion: lock.version,
|
|
144
|
+
summary: {
|
|
145
|
+
total: skills.length,
|
|
146
|
+
absent: skills.filter((skill) => skill.status === 'absent').length,
|
|
147
|
+
safeToRemove: skills.filter((skill) => skill.status === 'safe-to-remove').length,
|
|
148
|
+
conflict: skills.filter((skill) => skill.status === 'conflict').length,
|
|
149
|
+
},
|
|
150
|
+
skills,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function readSkillVersion(skillMarkdown) {
|
|
154
|
+
const frontmatter = skillMarkdown.replace(/\r\n/g, '\n').match(/^---\n([\s\S]*?)\n---/)?.[1];
|
|
155
|
+
if (!frontmatter)
|
|
156
|
+
return null;
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = parseYaml(frontmatter);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
if (!isRecord(parsed))
|
|
165
|
+
return null;
|
|
166
|
+
const metadataVersion = isRecord(parsed.metadata) ? parsed.metadata.version : undefined;
|
|
167
|
+
// Skills before 2.0 used a vendor-specific top-level `version`.
|
|
168
|
+
const version = metadataVersion ?? parsed.version;
|
|
169
|
+
return typeof version === 'string' && version.trim() ? version.trim() : null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Compares installed current Dinox skills against the skill set shipped with this CLI version.
|
|
173
|
+
* Installed skill files are read from the installer's canonical directory next to its lock.
|
|
174
|
+
*/
|
|
175
|
+
export async function scanCurrentSkills(lockFilePath = defaultLockFilePath(), readLockFile = defaultReadLockFile, readInstalledSkill = defaultReadLockFile, expectedVersion = getPackageVersion()) {
|
|
176
|
+
const lock = await readSkillsLock(lockFilePath, readLockFile);
|
|
177
|
+
const skillsRoot = path.join(path.dirname(lockFilePath), 'skills');
|
|
178
|
+
const skills = [];
|
|
179
|
+
for (const name of listRegisteredSkillNames()) {
|
|
180
|
+
const rawEntry = Object.prototype.hasOwnProperty.call(lock.entries, name) ? lock.entries[name] : undefined;
|
|
181
|
+
if (rawEntry === undefined) {
|
|
182
|
+
skills.push({ name, status: 'missing', source: null, installedVersion: null });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const source = isRecord(rawEntry) && typeof rawEntry.source === 'string' ? rawEntry.source : null;
|
|
186
|
+
if (source !== SKILLS_REPO_SLUG) {
|
|
187
|
+
skills.push({ name, status: 'foreign', source, installedVersion: null });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
let installedVersion = null;
|
|
191
|
+
try {
|
|
192
|
+
installedVersion = readSkillVersion(await readInstalledSkill(path.join(skillsRoot, name, 'SKILL.md')));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
installedVersion = null;
|
|
196
|
+
}
|
|
197
|
+
skills.push({
|
|
198
|
+
name,
|
|
199
|
+
status: installedVersion === null
|
|
200
|
+
? 'unknown-version'
|
|
201
|
+
: installedVersion === expectedVersion ? 'current' : 'outdated',
|
|
202
|
+
source,
|
|
203
|
+
installedVersion,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const count = (status) => skills.filter((skill) => skill.status === status).length;
|
|
207
|
+
const summary = {
|
|
208
|
+
total: skills.length,
|
|
209
|
+
current: count('current'),
|
|
210
|
+
outdated: count('outdated'),
|
|
211
|
+
missing: count('missing'),
|
|
212
|
+
foreign: count('foreign'),
|
|
213
|
+
'unknown-version': count('unknown-version'),
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
expectedVersion,
|
|
217
|
+
installCommand: getSkillsReleaseMetadata(expectedVersion).installCommand,
|
|
218
|
+
inSync: summary.current === summary.total,
|
|
219
|
+
summary,
|
|
220
|
+
skills,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function appendBoundedOutput(current, chunk) {
|
|
224
|
+
const currentBytes = Buffer.byteLength(current, 'utf8');
|
|
225
|
+
if (currentBytes >= MAX_CAPTURED_INSTALLER_OUTPUT_BYTES)
|
|
226
|
+
return current;
|
|
227
|
+
const remaining = MAX_CAPTURED_INSTALLER_OUTPUT_BYTES - currentBytes;
|
|
228
|
+
return current + chunk.subarray(0, remaining).toString('utf8');
|
|
229
|
+
}
|
|
230
|
+
export function resolveSkillsRemoveInvocation(skillNames, platform = process.platform) {
|
|
231
|
+
const args = [
|
|
232
|
+
'--yes',
|
|
233
|
+
`${SKILLS_INSTALLER_NAME}@${SKILLS_INSTALLER_VERSION}`,
|
|
234
|
+
'remove',
|
|
235
|
+
'-g',
|
|
236
|
+
...SKILLS_INSTALL_AGENTS.flatMap((agent) => ['-a', agent]),
|
|
237
|
+
'--skill',
|
|
238
|
+
...skillNames,
|
|
239
|
+
'-y',
|
|
240
|
+
];
|
|
241
|
+
const displayCommand = ['npx', ...args].join(' ');
|
|
242
|
+
if (platform === 'win32') {
|
|
243
|
+
return {
|
|
244
|
+
command: ['npx.cmd', ...args].join(' '),
|
|
245
|
+
args: [],
|
|
246
|
+
shell: true,
|
|
247
|
+
displayCommand,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
command: 'npx',
|
|
252
|
+
args,
|
|
253
|
+
shell: false,
|
|
254
|
+
displayCommand,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
export function runSkillsRemoval(skillNames) {
|
|
258
|
+
const invocation = resolveSkillsRemoveInvocation(skillNames);
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
let stdout = '';
|
|
261
|
+
let stderr = '';
|
|
262
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
263
|
+
shell: invocation.shell,
|
|
264
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
265
|
+
});
|
|
266
|
+
child.stdout.on('data', (chunk) => {
|
|
267
|
+
stdout = appendBoundedOutput(stdout, chunk);
|
|
268
|
+
});
|
|
269
|
+
child.stderr.on('data', (chunk) => {
|
|
270
|
+
stderr = appendBoundedOutput(stderr, chunk);
|
|
271
|
+
});
|
|
272
|
+
child.once('error', (error) => {
|
|
273
|
+
reject(new DinoxError(`Failed to start the skills installer: ${error.message}`, {
|
|
274
|
+
code: 'SKILLS_MIGRATION_FAILED',
|
|
275
|
+
cause: error,
|
|
276
|
+
details: {
|
|
277
|
+
command: invocation.displayCommand,
|
|
278
|
+
},
|
|
279
|
+
suggestion: 'Verify that Node.js and npx are available, then rerun `dino skills migrate --confirm --format json`.',
|
|
280
|
+
recoverable: true,
|
|
281
|
+
}));
|
|
282
|
+
});
|
|
283
|
+
child.once('close', (exitCode) => {
|
|
284
|
+
resolve({ invocation, exitCode, stdout, stderr });
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
function resolveDependencies(overrides) {
|
|
289
|
+
return {
|
|
290
|
+
getLockFilePath: defaultLockFilePath,
|
|
291
|
+
readLockFile: defaultReadLockFile,
|
|
292
|
+
readInstalledSkill: defaultReadLockFile,
|
|
293
|
+
getExpectedVersion: getPackageVersion,
|
|
294
|
+
runRemoval: runSkillsRemoval,
|
|
295
|
+
...overrides,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function replacementLabel(skill) {
|
|
299
|
+
if (skill.replacement)
|
|
300
|
+
return skill.replacement;
|
|
301
|
+
return skill.replacementNote ?? 'no direct replacement';
|
|
302
|
+
}
|
|
303
|
+
function printScan(scan) {
|
|
304
|
+
printHumanReadableLine(`skills lock: ${scan.lockFile} (${scan.lockFound ? 'found' : 'not found'})`);
|
|
305
|
+
for (const skill of scan.skills) {
|
|
306
|
+
if (skill.status === 'conflict') {
|
|
307
|
+
printHumanReadableLine(`${skill.name}: conflict; replacement: ${replacementLabel(skill)}; observed source=${skill.observed?.source ?? 'missing'} hash=${skill.observed?.skillFolderHash ?? 'missing'}`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
printHumanReadableLine(`${skill.name}: ${skill.status}; replacement: ${replacementLabel(skill)}`);
|
|
311
|
+
}
|
|
312
|
+
printHumanReadableLine(`summary: safe-to-remove=${scan.summary.safeToRemove} conflict=${scan.summary.conflict} absent=${scan.summary.absent}`);
|
|
313
|
+
}
|
|
314
|
+
function printCurrentScan(current) {
|
|
315
|
+
printHumanReadableLine(`current skills (expected ${current.expectedVersion}):`);
|
|
316
|
+
for (const skill of current.skills) {
|
|
317
|
+
const version = skill.installedVersion ? ` installed=${skill.installedVersion}` : '';
|
|
318
|
+
const source = skill.status === 'foreign' ? ` source=${skill.source ?? 'missing'}` : '';
|
|
319
|
+
printHumanReadableLine(`${skill.name}: ${skill.status}${version}${source}`);
|
|
320
|
+
}
|
|
321
|
+
if (!current.inSync) {
|
|
322
|
+
printHumanReadableLine(`to install the matching skills, run: ${current.installCommand}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function installerPayload(skillNames) {
|
|
326
|
+
return {
|
|
327
|
+
name: SKILLS_INSTALLER_NAME,
|
|
328
|
+
version: SKILLS_INSTALLER_VERSION,
|
|
329
|
+
scope: 'global',
|
|
330
|
+
agents: [...SKILLS_INSTALL_AGENTS],
|
|
331
|
+
command: skillNames.length > 0
|
|
332
|
+
? resolveSkillsRemoveInvocation(skillNames).displayCommand
|
|
333
|
+
: null,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function safeSkillNames(scan) {
|
|
337
|
+
return scan.skills
|
|
338
|
+
.filter((skill) => skill.status === 'safe-to-remove')
|
|
339
|
+
.map((skill) => skill.name);
|
|
340
|
+
}
|
|
341
|
+
function conflictSkillNames(scan) {
|
|
342
|
+
return scan.skills
|
|
343
|
+
.filter((skill) => skill.status === 'conflict')
|
|
344
|
+
.map((skill) => skill.name);
|
|
345
|
+
}
|
|
346
|
+
function scanStatus(scan, name) {
|
|
347
|
+
return scan.skills.find((skill) => skill.name === name)?.status ?? null;
|
|
348
|
+
}
|
|
349
|
+
async function rescanForVerification(dependencies, lockFilePath) {
|
|
350
|
+
try {
|
|
351
|
+
return await scanRetiredSkillsLock(lockFilePath, dependencies.readLockFile);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
throw new DinoxError('Failed to verify the retired skills migration', {
|
|
355
|
+
code: 'SKILLS_MIGRATION_VERIFICATION_FAILED',
|
|
356
|
+
cause: error,
|
|
357
|
+
details: { lockFile: lockFilePath },
|
|
358
|
+
suggestion: 'Run `dino skills doctor --format json` and inspect the lock before retrying.',
|
|
359
|
+
recoverable: true,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
export function createSkillsCommandGroup(overrides = {}) {
|
|
364
|
+
const dependencies = resolveDependencies(overrides);
|
|
365
|
+
return {
|
|
366
|
+
name: 'skills',
|
|
367
|
+
description: 'Inspect installed Dinox agent skills and safely migrate retired ones',
|
|
368
|
+
commands: [
|
|
369
|
+
{
|
|
370
|
+
name: 'doctor',
|
|
371
|
+
description: 'Check installed Dinox skills against this CLI version and find safely removable retired skills',
|
|
372
|
+
schema: {
|
|
373
|
+
path: 'skills.doctor',
|
|
374
|
+
risk: 'read',
|
|
375
|
+
auth: 'none',
|
|
376
|
+
globalOptions: FORMAT_OPTIONS,
|
|
377
|
+
output: {
|
|
378
|
+
type: 'structured',
|
|
379
|
+
summary: 'Retired skill status (summary, skills) and current skill version drift (current) from the global skills installer lock',
|
|
380
|
+
fields: ['lockFile', 'lockFound', 'lockVersion', 'summary', 'skills', 'current'],
|
|
381
|
+
},
|
|
382
|
+
examples: ['dino skills doctor', 'dino skills doctor --format json'],
|
|
383
|
+
},
|
|
384
|
+
action: async (_context) => {
|
|
385
|
+
const lockFilePath = dependencies.getLockFilePath();
|
|
386
|
+
const scan = await scanRetiredSkillsLock(lockFilePath, dependencies.readLockFile);
|
|
387
|
+
const current = await scanCurrentSkills(lockFilePath, dependencies.readLockFile, dependencies.readInstalledSkill, dependencies.getExpectedVersion());
|
|
388
|
+
const report = { ...scan, current };
|
|
389
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
390
|
+
printSuccess(report);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
printScan(scan);
|
|
394
|
+
printCurrentScan(current);
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: 'migrate',
|
|
399
|
+
description: 'Preview or confirm safe removal of retired Dinox skills',
|
|
400
|
+
options: [
|
|
401
|
+
{
|
|
402
|
+
flags: '--dry-run',
|
|
403
|
+
description: 'Preview safe removals without changing installed skills (default)',
|
|
404
|
+
type: 'boolean',
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
flags: '--confirm',
|
|
408
|
+
description: 'Execute the reviewed migration using the pinned public skills installer',
|
|
409
|
+
type: 'boolean',
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
schema: {
|
|
413
|
+
path: 'skills.migrate',
|
|
414
|
+
risk: 'high-risk-write',
|
|
415
|
+
auth: 'none',
|
|
416
|
+
globalOptions: FORMAT_OPTIONS,
|
|
417
|
+
output: {
|
|
418
|
+
type: 'structured',
|
|
419
|
+
summary: 'Retired skill migration plan or verified removal result',
|
|
420
|
+
fields: [
|
|
421
|
+
'mode',
|
|
422
|
+
'confirmed',
|
|
423
|
+
'installer',
|
|
424
|
+
'before',
|
|
425
|
+
'plannedRemoval',
|
|
426
|
+
'conflicts',
|
|
427
|
+
'removed',
|
|
428
|
+
'after',
|
|
429
|
+
'verified',
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
examples: [
|
|
433
|
+
'dino skills migrate --dry-run --format json',
|
|
434
|
+
'dino skills migrate --confirm --format json',
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
action: async (context) => {
|
|
438
|
+
const explicitDryRun = Boolean(context.options.dryRun);
|
|
439
|
+
const confirmed = Boolean(context.options.confirm);
|
|
440
|
+
if (explicitDryRun && confirmed) {
|
|
441
|
+
throw new DinoxError('--dry-run cannot be used together with --confirm', {
|
|
442
|
+
code: 'USAGE_ERROR',
|
|
443
|
+
suggestion: 'Run `dino skills migrate --dry-run --format json`, review the plan, then rerun with `--confirm`.',
|
|
444
|
+
recoverable: true,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
const lockFilePath = dependencies.getLockFilePath();
|
|
448
|
+
const before = await scanRetiredSkillsLock(lockFilePath, dependencies.readLockFile);
|
|
449
|
+
const plannedRemoval = safeSkillNames(before);
|
|
450
|
+
const conflicts = conflictSkillNames(before);
|
|
451
|
+
const installer = installerPayload(plannedRemoval);
|
|
452
|
+
if (!confirmed) {
|
|
453
|
+
const payload = {
|
|
454
|
+
mode: 'dry-run',
|
|
455
|
+
confirmed: false,
|
|
456
|
+
installer,
|
|
457
|
+
before,
|
|
458
|
+
plannedRemoval,
|
|
459
|
+
conflicts,
|
|
460
|
+
removed: [],
|
|
461
|
+
after: null,
|
|
462
|
+
verified: false,
|
|
463
|
+
};
|
|
464
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
465
|
+
printSuccess(payload);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
printHumanReadableLine('mode: dry-run');
|
|
469
|
+
printScan(before);
|
|
470
|
+
printHumanReadableLine(`would remove: ${plannedRemoval.join(', ') || 'none'}`);
|
|
471
|
+
printHumanReadableLine('no changes made; use --confirm only after reviewing this plan');
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (plannedRemoval.length === 0) {
|
|
475
|
+
const after = await rescanForVerification(dependencies, lockFilePath);
|
|
476
|
+
const payload = {
|
|
477
|
+
mode: 'migrate',
|
|
478
|
+
confirmed: true,
|
|
479
|
+
installer,
|
|
480
|
+
before,
|
|
481
|
+
plannedRemoval,
|
|
482
|
+
conflicts,
|
|
483
|
+
removed: [],
|
|
484
|
+
after,
|
|
485
|
+
verified: true,
|
|
486
|
+
};
|
|
487
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
488
|
+
printSuccess(payload);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
printHumanReadableLine('mode: migrate');
|
|
492
|
+
printScan(after);
|
|
493
|
+
printHumanReadableLine('removed: none');
|
|
494
|
+
printHumanReadableLine('verified: yes');
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const preExecution = await scanRetiredSkillsLock(lockFilePath, dependencies.readLockFile);
|
|
498
|
+
const changedCandidates = plannedRemoval.filter((name) => scanStatus(preExecution, name) !== 'safe-to-remove');
|
|
499
|
+
if (changedCandidates.length > 0) {
|
|
500
|
+
throw new DinoxError('The skills installer lock changed before migration execution', {
|
|
501
|
+
code: 'SKILLS_MIGRATION_PRECONDITION_FAILED',
|
|
502
|
+
exitCode: 5,
|
|
503
|
+
details: {
|
|
504
|
+
lockFile: lockFilePath,
|
|
505
|
+
changedCandidates,
|
|
506
|
+
before,
|
|
507
|
+
preExecution,
|
|
508
|
+
},
|
|
509
|
+
suggestion: 'Run `dino skills doctor --format json`, review the new status, then retry the migration.',
|
|
510
|
+
recoverable: true,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const execution = await dependencies.runRemoval(plannedRemoval);
|
|
514
|
+
const after = await rescanForVerification(dependencies, lockFilePath);
|
|
515
|
+
if (execution.exitCode !== 0) {
|
|
516
|
+
throw new DinoxError(`The skills installer exited with code ${execution.exitCode ?? 'unknown'}`, {
|
|
517
|
+
code: 'SKILLS_MIGRATION_FAILED',
|
|
518
|
+
details: {
|
|
519
|
+
command: execution.invocation.displayCommand,
|
|
520
|
+
exitCode: execution.exitCode,
|
|
521
|
+
...(execution.stdout.trim() ? { stdout: execution.stdout.trim() } : {}),
|
|
522
|
+
...(execution.stderr.trim() ? { stderr: execution.stderr.trim() } : {}),
|
|
523
|
+
after,
|
|
524
|
+
},
|
|
525
|
+
suggestion: 'Run `dino skills doctor --format json` before deciding whether to retry.',
|
|
526
|
+
recoverable: true,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
const remainingCandidates = plannedRemoval.filter((name) => scanStatus(after, name) !== 'absent');
|
|
530
|
+
const unexpectedlyRemovedConflicts = conflicts.filter((name) => scanStatus(after, name) === 'absent');
|
|
531
|
+
if (remainingCandidates.length > 0 || unexpectedlyRemovedConflicts.length > 0) {
|
|
532
|
+
throw new DinoxError('Retired skills migration verification failed', {
|
|
533
|
+
code: 'SKILLS_MIGRATION_VERIFICATION_FAILED',
|
|
534
|
+
details: {
|
|
535
|
+
command: execution.invocation.displayCommand,
|
|
536
|
+
remainingCandidates,
|
|
537
|
+
unexpectedlyRemovedConflicts,
|
|
538
|
+
before,
|
|
539
|
+
after,
|
|
540
|
+
},
|
|
541
|
+
suggestion: 'Run `dino skills doctor --format json` and inspect every conflict before taking further action.',
|
|
542
|
+
recoverable: true,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const payload = {
|
|
546
|
+
mode: 'migrate',
|
|
547
|
+
confirmed: true,
|
|
548
|
+
installer,
|
|
549
|
+
before,
|
|
550
|
+
plannedRemoval,
|
|
551
|
+
conflicts,
|
|
552
|
+
removed: [...plannedRemoval],
|
|
553
|
+
after,
|
|
554
|
+
verified: true,
|
|
555
|
+
};
|
|
556
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
557
|
+
printSuccess(payload);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
printHumanReadableLine('mode: migrate');
|
|
561
|
+
printHumanReadableLine(`removed: ${plannedRemoval.join(', ')}`);
|
|
562
|
+
printHumanReadableLine(`conflicts preserved: ${conflicts.join(', ') || 'none'}`);
|
|
563
|
+
printHumanReadableLine('verified: yes');
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
export const skillsCommandGroup = createSkillsCommandGroup();
|
|
570
|
+
export function registerSkillsCommands(program, overrides = {}) {
|
|
571
|
+
registerCommandGroup(program, createSkillsCommandGroup(overrides));
|
|
572
|
+
}
|
|
@@ -53,7 +53,7 @@ async function runStorageUpload(context) {
|
|
|
53
53
|
storageUrl: plan.storageUrl,
|
|
54
54
|
selectionSource: plan.selectionSource,
|
|
55
55
|
overwrite: options.overwrite === true,
|
|
56
|
-
}));
|
|
56
|
+
}), runtime);
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
const result = await uploadFileToStorage(db, {
|
|
@@ -117,7 +117,7 @@ async function runStorageTest(context) {
|
|
|
117
117
|
storageUrl: plan.storageUrl,
|
|
118
118
|
sizeBytes: plan.sizeBytes,
|
|
119
119
|
selectionSource: plan.selectionSource,
|
|
120
|
-
}));
|
|
120
|
+
}), runtime);
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
const result = await testStorageUpload(db, {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import { type StorageListResult, type StorageStatsResult, type StorageTestPlan, type StorageUploadCategory, type StorageUploadPlan, type StoredCustomStorageConfig } from './shared.js';
|
|
3
|
+
export declare function getTableColumns(db: AbstractPowerSyncDatabase, tableName: 'c_storage' | 'c_resource'): Promise<Set<string>>;
|
|
4
|
+
export declare function listStoredCustomStorageConfigs(db: AbstractPowerSyncDatabase, userId: string): Promise<StoredCustomStorageConfig[]>;
|
|
5
|
+
export declare function getActiveCustomStorageConfigId(db: AbstractPowerSyncDatabase, userId: string): Promise<string | null>;
|
|
6
|
+
export declare function resolveStorageSelection(args: {
|
|
7
|
+
configs: StoredCustomStorageConfig[];
|
|
8
|
+
storageId?: string;
|
|
9
|
+
activeCustomStorageConfigId: string | null;
|
|
10
|
+
}): {
|
|
11
|
+
config: StoredCustomStorageConfig;
|
|
12
|
+
selectionSource: StorageUploadPlan['selectionSource'];
|
|
13
|
+
};
|
|
14
|
+
export declare function buildStorageUploadPlan(args: {
|
|
15
|
+
config: StoredCustomStorageConfig;
|
|
16
|
+
selectionSource: StorageUploadPlan['selectionSource'];
|
|
17
|
+
userId: string;
|
|
18
|
+
filePath: string;
|
|
19
|
+
category: StorageUploadCategory;
|
|
20
|
+
key?: string;
|
|
21
|
+
sizeBytes: number;
|
|
22
|
+
}): StorageUploadPlan;
|
|
23
|
+
export declare function buildStorageTestPlan(args: {
|
|
24
|
+
config: StoredCustomStorageConfig;
|
|
25
|
+
selectionSource: StorageUploadPlan['selectionSource'];
|
|
26
|
+
userId: string;
|
|
27
|
+
}): StorageTestPlan;
|
|
28
|
+
export declare function parseStorageUploadCategory(value: unknown): StorageUploadCategory;
|
|
29
|
+
export declare function listStorageConfigs(db: AbstractPowerSyncDatabase, userId: string): Promise<StorageListResult>;
|
|
30
|
+
export declare function getStorageStats(db: AbstractPowerSyncDatabase, userId: string): Promise<StorageStatsResult>;
|