@dinoxx/dinox-cli 1.1.1 → 2.0.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.d.ts +9 -0
- package/dist/commands/notes/repo.js +65 -15
- 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/renderSkills.d.ts +2 -0
- package/dist/commands/schema/renderSkills.js +69 -10
- package/dist/commands/schema/spec.js +4 -0
- package/dist/commands/skills/index.d.ts +54 -0
- package/dist/commands/skills/index.js +468 -0
- package/dist/commands/storage/index.js +2 -2
- 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/daemon/client.d.ts +2 -2
- package/dist/daemon/client.js +106 -52
- 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/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/errors.d.ts +2 -0
- package/dist/utils/errors.js +2 -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 +8 -5
|
@@ -0,0 +1,468 @@
|
|
|
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 { SKILLS_INSTALL_AGENTS, SKILLS_INSTALLER_NAME, SKILLS_INSTALLER_VERSION, } from '../../constants/links.js';
|
|
6
|
+
import { RETIRED_SKILLS } from '../../skills/retired.js';
|
|
7
|
+
import { DinoxError } from '../../utils/errors.js';
|
|
8
|
+
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
9
|
+
import { registerCommandGroup } from '../descriptor/register.js';
|
|
10
|
+
import { FORMAT_OPTIONS } from '../schema/globalOptions.js';
|
|
11
|
+
const MAX_CAPTURED_INSTALLER_OUTPUT_BYTES = 64 * 1024;
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function defaultLockFilePath() {
|
|
16
|
+
return path.join(homedir(), '.agents', '.skill-lock.json');
|
|
17
|
+
}
|
|
18
|
+
async function defaultReadLockFile(lockFilePath) {
|
|
19
|
+
return readFile(lockFilePath, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
function absentScan(lockFilePath) {
|
|
22
|
+
const skills = RETIRED_SKILLS.map((skill) => ({
|
|
23
|
+
name: skill.name,
|
|
24
|
+
replacement: skill.replacement,
|
|
25
|
+
...('replacementNote' in skill ? { replacementNote: skill.replacementNote } : {}),
|
|
26
|
+
status: 'absent',
|
|
27
|
+
expected: {
|
|
28
|
+
source: skill.source,
|
|
29
|
+
skillFolderHash: skill.skillFolderHash,
|
|
30
|
+
},
|
|
31
|
+
observed: null,
|
|
32
|
+
mismatches: [],
|
|
33
|
+
}));
|
|
34
|
+
return {
|
|
35
|
+
lockFile: lockFilePath,
|
|
36
|
+
lockFound: false,
|
|
37
|
+
lockVersion: null,
|
|
38
|
+
summary: {
|
|
39
|
+
total: skills.length,
|
|
40
|
+
absent: skills.length,
|
|
41
|
+
safeToRemove: 0,
|
|
42
|
+
conflict: 0,
|
|
43
|
+
},
|
|
44
|
+
skills,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function invalidLockError(lockFilePath, message, cause) {
|
|
48
|
+
return new DinoxError(`Invalid skills installer lock at ${lockFilePath}: ${message}`, {
|
|
49
|
+
code: 'SKILLS_LOCK_INVALID',
|
|
50
|
+
cause,
|
|
51
|
+
details: { lockFile: lockFilePath },
|
|
52
|
+
suggestion: 'Inspect the lock file without deleting it, then rerun `dino skills doctor --format json`.',
|
|
53
|
+
recoverable: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export async function scanRetiredSkillsLock(lockFilePath = defaultLockFilePath(), readLockFile = defaultReadLockFile) {
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = await readLockFile(lockFilePath);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error?.code === 'ENOENT') {
|
|
63
|
+
return absentScan(lockFilePath);
|
|
64
|
+
}
|
|
65
|
+
throw new DinoxError(`Failed to read skills installer lock at ${lockFilePath}`, {
|
|
66
|
+
code: 'SKILLS_LOCK_READ_FAILED',
|
|
67
|
+
cause: error,
|
|
68
|
+
details: { lockFile: lockFilePath },
|
|
69
|
+
suggestion: 'Check file permissions, then rerun `dino skills doctor --format json`.',
|
|
70
|
+
recoverable: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(raw);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw invalidLockError(lockFilePath, 'file is not valid JSON', error);
|
|
79
|
+
}
|
|
80
|
+
if (!isRecord(parsed) || !isRecord(parsed.skills)) {
|
|
81
|
+
throw invalidLockError(lockFilePath, 'expected an object with a skills map');
|
|
82
|
+
}
|
|
83
|
+
const entries = parsed.skills;
|
|
84
|
+
const skills = RETIRED_SKILLS.map((skill) => {
|
|
85
|
+
if (!Object.prototype.hasOwnProperty.call(entries, skill.name)) {
|
|
86
|
+
return {
|
|
87
|
+
name: skill.name,
|
|
88
|
+
replacement: skill.replacement,
|
|
89
|
+
...('replacementNote' in skill ? { replacementNote: skill.replacementNote } : {}),
|
|
90
|
+
status: 'absent',
|
|
91
|
+
expected: {
|
|
92
|
+
source: skill.source,
|
|
93
|
+
skillFolderHash: skill.skillFolderHash,
|
|
94
|
+
},
|
|
95
|
+
observed: null,
|
|
96
|
+
mismatches: [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const rawEntry = entries[skill.name];
|
|
100
|
+
const entry = isRecord(rawEntry) ? rawEntry : {};
|
|
101
|
+
const observed = {
|
|
102
|
+
source: typeof entry.source === 'string' ? entry.source : null,
|
|
103
|
+
skillFolderHash: typeof entry.skillFolderHash === 'string' ? entry.skillFolderHash : null,
|
|
104
|
+
};
|
|
105
|
+
const mismatches = [];
|
|
106
|
+
if (observed.source !== skill.source)
|
|
107
|
+
mismatches.push('source');
|
|
108
|
+
if (observed.skillFolderHash !== skill.skillFolderHash)
|
|
109
|
+
mismatches.push('skillFolderHash');
|
|
110
|
+
return {
|
|
111
|
+
name: skill.name,
|
|
112
|
+
replacement: skill.replacement,
|
|
113
|
+
...('replacementNote' in skill ? { replacementNote: skill.replacementNote } : {}),
|
|
114
|
+
status: mismatches.length === 0 ? 'safe-to-remove' : 'conflict',
|
|
115
|
+
expected: {
|
|
116
|
+
source: skill.source,
|
|
117
|
+
skillFolderHash: skill.skillFolderHash,
|
|
118
|
+
},
|
|
119
|
+
observed,
|
|
120
|
+
mismatches,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
lockFile: lockFilePath,
|
|
125
|
+
lockFound: true,
|
|
126
|
+
lockVersion: typeof parsed.version === 'number' ? parsed.version : null,
|
|
127
|
+
summary: {
|
|
128
|
+
total: skills.length,
|
|
129
|
+
absent: skills.filter((skill) => skill.status === 'absent').length,
|
|
130
|
+
safeToRemove: skills.filter((skill) => skill.status === 'safe-to-remove').length,
|
|
131
|
+
conflict: skills.filter((skill) => skill.status === 'conflict').length,
|
|
132
|
+
},
|
|
133
|
+
skills,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function appendBoundedOutput(current, chunk) {
|
|
137
|
+
const currentBytes = Buffer.byteLength(current, 'utf8');
|
|
138
|
+
if (currentBytes >= MAX_CAPTURED_INSTALLER_OUTPUT_BYTES)
|
|
139
|
+
return current;
|
|
140
|
+
const remaining = MAX_CAPTURED_INSTALLER_OUTPUT_BYTES - currentBytes;
|
|
141
|
+
return current + chunk.subarray(0, remaining).toString('utf8');
|
|
142
|
+
}
|
|
143
|
+
export function resolveSkillsRemoveInvocation(skillNames, platform = process.platform) {
|
|
144
|
+
const args = [
|
|
145
|
+
'--yes',
|
|
146
|
+
`${SKILLS_INSTALLER_NAME}@${SKILLS_INSTALLER_VERSION}`,
|
|
147
|
+
'remove',
|
|
148
|
+
'-g',
|
|
149
|
+
...SKILLS_INSTALL_AGENTS.flatMap((agent) => ['-a', agent]),
|
|
150
|
+
'--skill',
|
|
151
|
+
...skillNames,
|
|
152
|
+
'-y',
|
|
153
|
+
];
|
|
154
|
+
const displayCommand = ['npx', ...args].join(' ');
|
|
155
|
+
if (platform === 'win32') {
|
|
156
|
+
return {
|
|
157
|
+
command: ['npx.cmd', ...args].join(' '),
|
|
158
|
+
args: [],
|
|
159
|
+
shell: true,
|
|
160
|
+
displayCommand,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
command: 'npx',
|
|
165
|
+
args,
|
|
166
|
+
shell: false,
|
|
167
|
+
displayCommand,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function runSkillsRemoval(skillNames) {
|
|
171
|
+
const invocation = resolveSkillsRemoveInvocation(skillNames);
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
let stdout = '';
|
|
174
|
+
let stderr = '';
|
|
175
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
176
|
+
shell: invocation.shell,
|
|
177
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
178
|
+
});
|
|
179
|
+
child.stdout.on('data', (chunk) => {
|
|
180
|
+
stdout = appendBoundedOutput(stdout, chunk);
|
|
181
|
+
});
|
|
182
|
+
child.stderr.on('data', (chunk) => {
|
|
183
|
+
stderr = appendBoundedOutput(stderr, chunk);
|
|
184
|
+
});
|
|
185
|
+
child.once('error', (error) => {
|
|
186
|
+
reject(new DinoxError(`Failed to start the skills installer: ${error.message}`, {
|
|
187
|
+
code: 'SKILLS_MIGRATION_FAILED',
|
|
188
|
+
cause: error,
|
|
189
|
+
details: {
|
|
190
|
+
command: invocation.displayCommand,
|
|
191
|
+
},
|
|
192
|
+
suggestion: 'Verify that Node.js and npx are available, then rerun `dino skills migrate --confirm --format json`.',
|
|
193
|
+
recoverable: true,
|
|
194
|
+
}));
|
|
195
|
+
});
|
|
196
|
+
child.once('close', (exitCode) => {
|
|
197
|
+
resolve({ invocation, exitCode, stdout, stderr });
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function resolveDependencies(overrides) {
|
|
202
|
+
return {
|
|
203
|
+
getLockFilePath: defaultLockFilePath,
|
|
204
|
+
readLockFile: defaultReadLockFile,
|
|
205
|
+
runRemoval: runSkillsRemoval,
|
|
206
|
+
...overrides,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function replacementLabel(skill) {
|
|
210
|
+
if (skill.replacement)
|
|
211
|
+
return skill.replacement;
|
|
212
|
+
return skill.replacementNote ?? 'no direct replacement';
|
|
213
|
+
}
|
|
214
|
+
function printScan(scan) {
|
|
215
|
+
printHumanReadableLine(`skills lock: ${scan.lockFile} (${scan.lockFound ? 'found' : 'not found'})`);
|
|
216
|
+
for (const skill of scan.skills) {
|
|
217
|
+
if (skill.status === 'conflict') {
|
|
218
|
+
printHumanReadableLine(`${skill.name}: conflict; replacement: ${replacementLabel(skill)}; observed source=${skill.observed?.source ?? 'missing'} hash=${skill.observed?.skillFolderHash ?? 'missing'}`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
printHumanReadableLine(`${skill.name}: ${skill.status}; replacement: ${replacementLabel(skill)}`);
|
|
222
|
+
}
|
|
223
|
+
printHumanReadableLine(`summary: safe-to-remove=${scan.summary.safeToRemove} conflict=${scan.summary.conflict} absent=${scan.summary.absent}`);
|
|
224
|
+
}
|
|
225
|
+
function installerPayload(skillNames) {
|
|
226
|
+
return {
|
|
227
|
+
name: SKILLS_INSTALLER_NAME,
|
|
228
|
+
version: SKILLS_INSTALLER_VERSION,
|
|
229
|
+
scope: 'global',
|
|
230
|
+
agents: [...SKILLS_INSTALL_AGENTS],
|
|
231
|
+
command: skillNames.length > 0
|
|
232
|
+
? resolveSkillsRemoveInvocation(skillNames).displayCommand
|
|
233
|
+
: null,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function safeSkillNames(scan) {
|
|
237
|
+
return scan.skills
|
|
238
|
+
.filter((skill) => skill.status === 'safe-to-remove')
|
|
239
|
+
.map((skill) => skill.name);
|
|
240
|
+
}
|
|
241
|
+
function conflictSkillNames(scan) {
|
|
242
|
+
return scan.skills
|
|
243
|
+
.filter((skill) => skill.status === 'conflict')
|
|
244
|
+
.map((skill) => skill.name);
|
|
245
|
+
}
|
|
246
|
+
function scanStatus(scan, name) {
|
|
247
|
+
return scan.skills.find((skill) => skill.name === name)?.status ?? null;
|
|
248
|
+
}
|
|
249
|
+
async function rescanForVerification(dependencies, lockFilePath) {
|
|
250
|
+
try {
|
|
251
|
+
return await scanRetiredSkillsLock(lockFilePath, dependencies.readLockFile);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
throw new DinoxError('Failed to verify the retired skills migration', {
|
|
255
|
+
code: 'SKILLS_MIGRATION_VERIFICATION_FAILED',
|
|
256
|
+
cause: error,
|
|
257
|
+
details: { lockFile: lockFilePath },
|
|
258
|
+
suggestion: 'Run `dino skills doctor --format json` and inspect the lock before retrying.',
|
|
259
|
+
recoverable: true,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
export function createSkillsCommandGroup(overrides = {}) {
|
|
264
|
+
const dependencies = resolveDependencies(overrides);
|
|
265
|
+
return {
|
|
266
|
+
name: 'skills',
|
|
267
|
+
description: 'Inspect and safely migrate installed Dinox agent skills',
|
|
268
|
+
commands: [
|
|
269
|
+
{
|
|
270
|
+
name: 'doctor',
|
|
271
|
+
description: 'Inspect the skills installer lock for safely removable retired Dinox skills',
|
|
272
|
+
schema: {
|
|
273
|
+
path: 'skills.doctor',
|
|
274
|
+
risk: 'read',
|
|
275
|
+
auth: 'none',
|
|
276
|
+
globalOptions: FORMAT_OPTIONS,
|
|
277
|
+
output: {
|
|
278
|
+
type: 'structured',
|
|
279
|
+
summary: 'Retired skill status from the global skills installer lock',
|
|
280
|
+
fields: ['lockFile', 'lockFound', 'lockVersion', 'summary', 'skills'],
|
|
281
|
+
},
|
|
282
|
+
examples: ['dino skills doctor', 'dino skills doctor --format json'],
|
|
283
|
+
},
|
|
284
|
+
action: async (_context) => {
|
|
285
|
+
const scan = await scanRetiredSkillsLock(dependencies.getLockFilePath(), dependencies.readLockFile);
|
|
286
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
287
|
+
printSuccess(scan);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
printScan(scan);
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'migrate',
|
|
295
|
+
description: 'Preview or confirm safe removal of retired Dinox skills',
|
|
296
|
+
options: [
|
|
297
|
+
{
|
|
298
|
+
flags: '--dry-run',
|
|
299
|
+
description: 'Preview safe removals without changing installed skills (default)',
|
|
300
|
+
type: 'boolean',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
flags: '--confirm',
|
|
304
|
+
description: 'Execute the reviewed migration using the pinned public skills installer',
|
|
305
|
+
type: 'boolean',
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
schema: {
|
|
309
|
+
path: 'skills.migrate',
|
|
310
|
+
risk: 'high-risk-write',
|
|
311
|
+
auth: 'none',
|
|
312
|
+
globalOptions: FORMAT_OPTIONS,
|
|
313
|
+
output: {
|
|
314
|
+
type: 'structured',
|
|
315
|
+
summary: 'Retired skill migration plan or verified removal result',
|
|
316
|
+
fields: [
|
|
317
|
+
'mode',
|
|
318
|
+
'confirmed',
|
|
319
|
+
'installer',
|
|
320
|
+
'before',
|
|
321
|
+
'plannedRemoval',
|
|
322
|
+
'conflicts',
|
|
323
|
+
'removed',
|
|
324
|
+
'after',
|
|
325
|
+
'verified',
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
examples: [
|
|
329
|
+
'dino skills migrate --dry-run --format json',
|
|
330
|
+
'dino skills migrate --confirm --format json',
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
action: async (context) => {
|
|
334
|
+
const explicitDryRun = Boolean(context.options.dryRun);
|
|
335
|
+
const confirmed = Boolean(context.options.confirm);
|
|
336
|
+
if (explicitDryRun && confirmed) {
|
|
337
|
+
throw new DinoxError('--dry-run cannot be used together with --confirm', {
|
|
338
|
+
code: 'USAGE_ERROR',
|
|
339
|
+
suggestion: 'Run `dino skills migrate --dry-run --format json`, review the plan, then rerun with `--confirm`.',
|
|
340
|
+
recoverable: true,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const lockFilePath = dependencies.getLockFilePath();
|
|
344
|
+
const before = await scanRetiredSkillsLock(lockFilePath, dependencies.readLockFile);
|
|
345
|
+
const plannedRemoval = safeSkillNames(before);
|
|
346
|
+
const conflicts = conflictSkillNames(before);
|
|
347
|
+
const installer = installerPayload(plannedRemoval);
|
|
348
|
+
if (!confirmed) {
|
|
349
|
+
const payload = {
|
|
350
|
+
mode: 'dry-run',
|
|
351
|
+
confirmed: false,
|
|
352
|
+
installer,
|
|
353
|
+
before,
|
|
354
|
+
plannedRemoval,
|
|
355
|
+
conflicts,
|
|
356
|
+
removed: [],
|
|
357
|
+
after: null,
|
|
358
|
+
verified: false,
|
|
359
|
+
};
|
|
360
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
361
|
+
printSuccess(payload);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
printHumanReadableLine('mode: dry-run');
|
|
365
|
+
printScan(before);
|
|
366
|
+
printHumanReadableLine(`would remove: ${plannedRemoval.join(', ') || 'none'}`);
|
|
367
|
+
printHumanReadableLine('no changes made; use --confirm only after reviewing this plan');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (plannedRemoval.length === 0) {
|
|
371
|
+
const after = await rescanForVerification(dependencies, lockFilePath);
|
|
372
|
+
const payload = {
|
|
373
|
+
mode: 'migrate',
|
|
374
|
+
confirmed: true,
|
|
375
|
+
installer,
|
|
376
|
+
before,
|
|
377
|
+
plannedRemoval,
|
|
378
|
+
conflicts,
|
|
379
|
+
removed: [],
|
|
380
|
+
after,
|
|
381
|
+
verified: true,
|
|
382
|
+
};
|
|
383
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
384
|
+
printSuccess(payload);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
printHumanReadableLine('mode: migrate');
|
|
388
|
+
printScan(after);
|
|
389
|
+
printHumanReadableLine('removed: none');
|
|
390
|
+
printHumanReadableLine('verified: yes');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const preExecution = await scanRetiredSkillsLock(lockFilePath, dependencies.readLockFile);
|
|
394
|
+
const changedCandidates = plannedRemoval.filter((name) => scanStatus(preExecution, name) !== 'safe-to-remove');
|
|
395
|
+
if (changedCandidates.length > 0) {
|
|
396
|
+
throw new DinoxError('The skills installer lock changed before migration execution', {
|
|
397
|
+
code: 'SKILLS_MIGRATION_PRECONDITION_FAILED',
|
|
398
|
+
exitCode: 5,
|
|
399
|
+
details: {
|
|
400
|
+
lockFile: lockFilePath,
|
|
401
|
+
changedCandidates,
|
|
402
|
+
before,
|
|
403
|
+
preExecution,
|
|
404
|
+
},
|
|
405
|
+
suggestion: 'Run `dino skills doctor --format json`, review the new status, then retry the migration.',
|
|
406
|
+
recoverable: true,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
const execution = await dependencies.runRemoval(plannedRemoval);
|
|
410
|
+
const after = await rescanForVerification(dependencies, lockFilePath);
|
|
411
|
+
if (execution.exitCode !== 0) {
|
|
412
|
+
throw new DinoxError(`The skills installer exited with code ${execution.exitCode ?? 'unknown'}`, {
|
|
413
|
+
code: 'SKILLS_MIGRATION_FAILED',
|
|
414
|
+
details: {
|
|
415
|
+
command: execution.invocation.displayCommand,
|
|
416
|
+
exitCode: execution.exitCode,
|
|
417
|
+
...(execution.stdout.trim() ? { stdout: execution.stdout.trim() } : {}),
|
|
418
|
+
...(execution.stderr.trim() ? { stderr: execution.stderr.trim() } : {}),
|
|
419
|
+
after,
|
|
420
|
+
},
|
|
421
|
+
suggestion: 'Run `dino skills doctor --format json` before deciding whether to retry.',
|
|
422
|
+
recoverable: true,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
const remainingCandidates = plannedRemoval.filter((name) => scanStatus(after, name) !== 'absent');
|
|
426
|
+
const unexpectedlyRemovedConflicts = conflicts.filter((name) => scanStatus(after, name) === 'absent');
|
|
427
|
+
if (remainingCandidates.length > 0 || unexpectedlyRemovedConflicts.length > 0) {
|
|
428
|
+
throw new DinoxError('Retired skills migration verification failed', {
|
|
429
|
+
code: 'SKILLS_MIGRATION_VERIFICATION_FAILED',
|
|
430
|
+
details: {
|
|
431
|
+
command: execution.invocation.displayCommand,
|
|
432
|
+
remainingCandidates,
|
|
433
|
+
unexpectedlyRemovedConflicts,
|
|
434
|
+
before,
|
|
435
|
+
after,
|
|
436
|
+
},
|
|
437
|
+
suggestion: 'Run `dino skills doctor --format json` and inspect every conflict before taking further action.',
|
|
438
|
+
recoverable: true,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
const payload = {
|
|
442
|
+
mode: 'migrate',
|
|
443
|
+
confirmed: true,
|
|
444
|
+
installer,
|
|
445
|
+
before,
|
|
446
|
+
plannedRemoval,
|
|
447
|
+
conflicts,
|
|
448
|
+
removed: [...plannedRemoval],
|
|
449
|
+
after,
|
|
450
|
+
verified: true,
|
|
451
|
+
};
|
|
452
|
+
if (getRequestedStructuredOutputFormat()) {
|
|
453
|
+
printSuccess(payload);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
printHumanReadableLine('mode: migrate');
|
|
457
|
+
printHumanReadableLine(`removed: ${plannedRemoval.join(', ')}`);
|
|
458
|
+
printHumanReadableLine(`conflicts preserved: ${conflicts.join(', ') || 'none'}`);
|
|
459
|
+
printHumanReadableLine('verified: yes');
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
export const skillsCommandGroup = createSkillsCommandGroup();
|
|
466
|
+
export function registerSkillsCommands(program, overrides = {}) {
|
|
467
|
+
registerCommandGroup(program, createSkillsCommandGroup(overrides));
|
|
468
|
+
}
|
|
@@ -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, {
|
package/dist/commands/sync.js
CHANGED
|
@@ -42,7 +42,8 @@ export async function executeSyncCommand(context, overrides = {}) {
|
|
|
42
42
|
config,
|
|
43
43
|
offline: false,
|
|
44
44
|
timeoutMs: remainingTimeoutMs(),
|
|
45
|
-
deadlineAtMs
|
|
45
|
+
deadlineAtMs,
|
|
46
|
+
operationDeadlineAtMs,
|
|
46
47
|
waitForInitialSync: false,
|
|
47
48
|
expectedTokenFingerprint: config.auth.tokenFingerprint ?? null,
|
|
48
49
|
cleanupFailureMode: 'throw',
|
|
@@ -6,6 +6,7 @@ import { printDryRunPreview } from '../../utils/dryRun.js';
|
|
|
6
6
|
import { DinoxError } from '../../utils/errors.js';
|
|
7
7
|
import { getRequestedStructuredOutputFormat, printHumanReadableLine, printSuccess, } from '../../utils/output.js';
|
|
8
8
|
import { commandStale, printStaleCacheWarning, syncBeforeCommand, waitForMutationUpload, withCommandRuntime, } from '../runtime.js';
|
|
9
|
+
import { assertWriteConfirmationProvided, parseExpectedCount } from '../writeConfirmation.js';
|
|
9
10
|
import { addTag, inspectAddTag, listTags } from './repo.js';
|
|
10
11
|
import { getTagStats, getTagTree, inspectTagCleanup, mergeTag, moveTag, renameTag, suggestTagMerges, } from './operations.js';
|
|
11
12
|
const TAG_GLOBAL_OPTIONS = [...FORMAT_OPTIONS, ...OFFLINE_OPTIONS, ...REQUIRE_SYNC_OPTIONS, ...SYNC_TIMEOUT_OPTIONS];
|
|
@@ -67,7 +68,7 @@ async function runTagAdd(context) {
|
|
|
67
68
|
userId: identity.userId,
|
|
68
69
|
emoji,
|
|
69
70
|
})),
|
|
70
|
-
}));
|
|
71
|
+
}), runtime);
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
73
74
|
const result = await addTag(runtime.db, {
|
|
@@ -114,7 +115,7 @@ async function runTagRename(context) {
|
|
|
114
115
|
summary: `Rename tag "${result.fromPath}" to "${result.toPath}".`,
|
|
115
116
|
inputs: { path, new_name: newName },
|
|
116
117
|
changes: { updated_tags: result.updatedTags, updated_notes: result.updatedNotes },
|
|
117
|
-
}));
|
|
118
|
+
}), runtime);
|
|
118
119
|
return;
|
|
119
120
|
}
|
|
120
121
|
await waitForMutationUpload(runtime);
|
|
@@ -139,7 +140,7 @@ async function runTagMove(context) {
|
|
|
139
140
|
summary: `Move tag "${result.fromPath}" to "${result.toPath}".`,
|
|
140
141
|
inputs: { path, to },
|
|
141
142
|
changes: { updated_tags: result.updatedTags, updated_notes: result.updatedNotes },
|
|
142
|
-
}));
|
|
143
|
+
}), runtime);
|
|
143
144
|
return;
|
|
144
145
|
}
|
|
145
146
|
await waitForMutationUpload(runtime);
|
|
@@ -149,9 +150,17 @@ async function runTagMove(context) {
|
|
|
149
150
|
async function runTagMerge(context) {
|
|
150
151
|
const from = requireArg(context.args.from, 'Source tag path is required');
|
|
151
152
|
const to = requireArg(context.args.to, 'Target tag path is required');
|
|
153
|
+
const dryRun = Boolean(context.options.dryRun);
|
|
154
|
+
const expectedCount = parseExpectedCount(context.options.expectedCount);
|
|
155
|
+
const writeOptions = {
|
|
156
|
+
dryRun,
|
|
157
|
+
confirm: Boolean(context.options.confirm),
|
|
158
|
+
expectedCount,
|
|
159
|
+
};
|
|
160
|
+
assertWriteConfirmationProvided('Tag merge', writeOptions);
|
|
152
161
|
await withTagWriteRuntime(context, async ({ runtime, userId }) => {
|
|
153
|
-
const result = await mergeTag(runtime.db, { from, to, userId },
|
|
154
|
-
if (
|
|
162
|
+
const result = await mergeTag(runtime.db, { from, to, userId }, writeOptions);
|
|
163
|
+
if (dryRun) {
|
|
155
164
|
printDryRunPreview(buildTagMutationDryRunPreview({
|
|
156
165
|
config: runtime.config,
|
|
157
166
|
offline: runtime.offline,
|
|
@@ -159,12 +168,15 @@ async function runTagMerge(context) {
|
|
|
159
168
|
action: 'merge_tag',
|
|
160
169
|
risk: 'high-risk-write',
|
|
161
170
|
summary: `Merge tag subtree "${result.fromPath}" into "${result.toPath}".`,
|
|
162
|
-
inputs: { from, to },
|
|
171
|
+
inputs: { from, to, expected_count: expectedCount },
|
|
163
172
|
changes: {
|
|
173
|
+
affected_count: result.affectedCount,
|
|
174
|
+
soft_deleted_tag_count: result.softDeletedTagIds.length,
|
|
175
|
+
updated_note_count: result.updatedNotes.length,
|
|
164
176
|
soft_deleted_tag_ids: result.softDeletedTagIds,
|
|
165
177
|
updated_notes: result.updatedNotes,
|
|
166
178
|
},
|
|
167
|
-
}));
|
|
179
|
+
}), runtime);
|
|
168
180
|
return;
|
|
169
181
|
}
|
|
170
182
|
await waitForMutationUpload(runtime);
|
|
@@ -193,7 +205,7 @@ async function runTagCleanup(context) {
|
|
|
193
205
|
summary: `Inspect tag cleanup candidates (${result.issues.length} issues).`,
|
|
194
206
|
inputs: {},
|
|
195
207
|
targets: result.issues,
|
|
196
|
-
}));
|
|
208
|
+
}), runtime);
|
|
197
209
|
return;
|
|
198
210
|
}
|
|
199
211
|
printSuccess({ stale: commandStale(runtime), ...result });
|
|
@@ -328,7 +340,11 @@ export const tagCommandGroup = {
|
|
|
328
340
|
{ syntax: '<from>', description: 'Source tag path' },
|
|
329
341
|
{ syntax: '<to>', description: 'Target tag path' },
|
|
330
342
|
],
|
|
331
|
-
options: [
|
|
343
|
+
options: [
|
|
344
|
+
{ flags: '--expected-count <n>', description: 'Required for real writes; must equal source tags plus notes affected', type: 'number' },
|
|
345
|
+
{ flags: '--confirm', description: 'Required for real writes after reviewing a dry run', type: 'boolean' },
|
|
346
|
+
{ flags: '--dry-run', description: 'Preview the write without executing it', type: 'boolean' },
|
|
347
|
+
],
|
|
332
348
|
schema: {
|
|
333
349
|
path: 'tag.merge',
|
|
334
350
|
risk: 'high-risk-write',
|
|
@@ -337,9 +353,12 @@ export const tagCommandGroup = {
|
|
|
337
353
|
output: {
|
|
338
354
|
type: 'structured',
|
|
339
355
|
summary: 'Tag merge result',
|
|
340
|
-
fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedTagIds', 'updatedNotes', 'stale'],
|
|
356
|
+
fields: ['fromId', 'toId', 'fromPath', 'toPath', 'softDeletedTagIds', 'updatedNotes', 'affectedCount', 'stale'],
|
|
341
357
|
},
|
|
342
|
-
examples: [
|
|
358
|
+
examples: [
|
|
359
|
+
'dino tag merge "ai" "AI" --format json --dry-run',
|
|
360
|
+
'dino tag merge "ai" "AI" --expected-count 4 --confirm --format json',
|
|
361
|
+
],
|
|
343
362
|
},
|
|
344
363
|
action: runTagMerge,
|
|
345
364
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AbstractPowerSyncDatabase } from '@powersync/common';
|
|
2
|
+
import { type ConfirmedWriteOptions } from '../writeConfirmation.js';
|
|
2
3
|
import type { TagListItem } from './model.js';
|
|
3
4
|
export type TagTreeNode = TagListItem & {
|
|
4
5
|
isActual: boolean;
|
|
@@ -63,6 +64,7 @@ export type TagMergeResult = {
|
|
|
63
64
|
before: string[];
|
|
64
65
|
after: string[];
|
|
65
66
|
}>;
|
|
67
|
+
affectedCount: number;
|
|
66
68
|
};
|
|
67
69
|
export type TagSuggestResult = {
|
|
68
70
|
suggestions: Array<{
|
|
@@ -98,8 +100,6 @@ export declare function renameTag(db: AbstractPowerSyncDatabase, input: TagRenam
|
|
|
98
100
|
export declare function moveTag(db: AbstractPowerSyncDatabase, input: TagMoveInput, options?: {
|
|
99
101
|
dryRun?: boolean;
|
|
100
102
|
}): Promise<TagRenameMoveResult>;
|
|
101
|
-
export declare function mergeTag(db: AbstractPowerSyncDatabase, input: TagMergeInput, options?:
|
|
102
|
-
dryRun?: boolean;
|
|
103
|
-
}): Promise<TagMergeResult>;
|
|
103
|
+
export declare function mergeTag(db: AbstractPowerSyncDatabase, input: TagMergeInput, options?: ConfirmedWriteOptions): Promise<TagMergeResult>;
|
|
104
104
|
export declare function inspectTagCleanup(db: AbstractPowerSyncDatabase, input?: TagReadInput): Promise<TagCleanupResult>;
|
|
105
105
|
export declare function suggestTagMerges(db: AbstractPowerSyncDatabase, input?: TagReadInput): Promise<TagSuggestResult>;
|