@fredlackey/devutils 0.0.19 → 0.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 +223 -32
- package/package.json +7 -5
- package/src/api/loader.js +229 -0
- package/src/api/registry.json +62 -0
- package/src/cli.js +305 -0
- package/src/commands/ai/index.js +16 -0
- package/src/commands/ai/launch.js +112 -0
- package/src/commands/ai/list.js +54 -0
- package/src/commands/ai/resume.js +70 -0
- package/src/commands/ai/sessions.js +121 -0
- package/src/commands/ai/set.js +131 -0
- package/src/commands/ai/show.js +74 -0
- package/src/commands/ai/tools.js +46 -0
- package/src/commands/alias/add.js +93 -0
- package/src/commands/alias/helpers.js +107 -0
- package/src/commands/alias/index.js +14 -0
- package/src/commands/alias/list.js +55 -0
- package/src/commands/alias/remove.js +62 -0
- package/src/commands/alias/sync.js +109 -0
- package/src/commands/api/disable.js +73 -0
- package/src/commands/api/enable.js +148 -0
- package/src/commands/api/index.js +15 -0
- package/src/commands/api/list.js +66 -0
- package/src/commands/api/update.js +87 -0
- package/src/commands/auth/index.js +15 -0
- package/src/commands/auth/list.js +49 -0
- package/src/commands/auth/login.js +384 -0
- package/src/commands/auth/logout.js +111 -0
- package/src/commands/auth/refresh.js +184 -0
- package/src/commands/auth/services.js +169 -0
- package/src/commands/auth/status.js +104 -0
- package/src/commands/config/export.js +224 -0
- package/src/commands/config/get.js +52 -0
- package/src/commands/config/import.js +308 -0
- package/src/commands/config/index.js +17 -0
- package/src/commands/config/init.js +143 -0
- package/src/commands/config/reset.js +57 -0
- package/src/commands/config/set.js +93 -0
- package/src/commands/config/show.js +35 -0
- package/src/commands/help.js +338 -0
- package/src/commands/identity/add.js +133 -0
- package/src/commands/identity/index.js +17 -0
- package/src/commands/identity/link.js +76 -0
- package/src/commands/identity/list.js +48 -0
- package/src/commands/identity/remove.js +72 -0
- package/src/commands/identity/show.js +65 -0
- package/src/commands/identity/sync.js +172 -0
- package/src/commands/identity/unlink.js +57 -0
- package/src/commands/ignore/add.js +165 -0
- package/src/commands/ignore/index.js +14 -0
- package/src/commands/ignore/list.js +89 -0
- package/src/commands/ignore/markers.js +43 -0
- package/src/commands/ignore/remove.js +164 -0
- package/src/commands/ignore/show.js +169 -0
- package/src/commands/machine/detect.js +122 -0
- package/src/commands/machine/index.js +14 -0
- package/src/commands/machine/list.js +74 -0
- package/src/commands/machine/set.js +106 -0
- package/src/commands/machine/show.js +35 -0
- package/src/commands/schema.js +152 -0
- package/src/commands/search/collections.js +134 -0
- package/src/commands/search/get.js +71 -0
- package/src/commands/search/index-cmd.js +54 -0
- package/src/commands/search/index.js +21 -0
- package/src/commands/search/keyword.js +60 -0
- package/src/commands/search/qmd.js +70 -0
- package/src/commands/search/query.js +64 -0
- package/src/commands/search/semantic.js +62 -0
- package/src/commands/search/status.js +46 -0
- package/src/commands/status.js +276 -0
- package/src/commands/tools/check.js +79 -0
- package/src/commands/tools/index.js +14 -0
- package/src/commands/tools/install.js +110 -0
- package/src/commands/tools/list.js +91 -0
- package/src/commands/tools/search.js +60 -0
- package/src/commands/update.js +113 -0
- package/src/commands/util/add.js +151 -0
- package/src/commands/util/index.js +15 -0
- package/src/commands/util/list.js +97 -0
- package/src/commands/util/remove.js +76 -0
- package/src/commands/util/run.js +79 -0
- package/src/commands/util/show.js +67 -0
- package/src/commands/version.js +33 -0
- package/src/installers/_template.js +104 -0
- package/src/installers/git.js +150 -0
- package/src/installers/homebrew.js +190 -0
- package/src/installers/node.js +223 -0
- package/src/installers/registry.json +29 -0
- package/src/lib/config.js +125 -0
- package/src/lib/detect.js +74 -0
- package/src/lib/errors.js +114 -0
- package/src/lib/github.js +315 -0
- package/src/lib/installer.js +225 -0
- package/src/lib/output.js +239 -0
- package/src/lib/platform.js +112 -0
- package/src/lib/platforms/amazon-linux.js +41 -0
- package/src/lib/platforms/gitbash.js +46 -0
- package/src/lib/platforms/macos.js +45 -0
- package/src/lib/platforms/raspbian.js +41 -0
- package/src/lib/platforms/ubuntu.js +39 -0
- package/src/lib/platforms/windows.js +45 -0
- package/src/lib/prompt.js +161 -0
- package/src/lib/schema.js +211 -0
- package/src/lib/shell.js +75 -0
- package/src/patterns/gitignore/claude-code.txt +25 -0
- package/src/patterns/gitignore/docker.txt +15 -0
- package/src/patterns/gitignore/go.txt +24 -0
- package/src/patterns/gitignore/java.txt +38 -0
- package/src/patterns/gitignore/jetbrains.txt +26 -0
- package/src/patterns/gitignore/linux.txt +18 -0
- package/src/patterns/gitignore/macos.txt +27 -0
- package/src/patterns/gitignore/node.txt +51 -0
- package/src/patterns/gitignore/python.txt +55 -0
- package/src/patterns/gitignore/rust.txt +14 -0
- package/src/patterns/gitignore/terraform.txt +30 -0
- package/src/patterns/gitignore/vscode.txt +15 -0
- package/src/patterns/gitignore/windows.txt +25 -0
- package/src/utils/clone/index.js +165 -0
- package/src/utils/git-push/index.js +230 -0
- package/src/utils/git-status/index.js +116 -0
- package/src/utils/git-status/unix.sh +75 -0
- package/src/utils/registry.json +41 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const meta = {
|
|
4
|
+
description: 'Introspect a command by dot-notation path. Returns description, arguments, and flags.',
|
|
5
|
+
arguments: [
|
|
6
|
+
{ name: 'path', required: false, description: 'Dot-notation path (e.g., config.set, api.gmail.messages.list)' },
|
|
7
|
+
],
|
|
8
|
+
flags: [],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
async function run(args, context) {
|
|
12
|
+
const schema = require('../lib/schema');
|
|
13
|
+
const dotPath = args.positional[0];
|
|
14
|
+
|
|
15
|
+
if (!dotPath) {
|
|
16
|
+
// No path given: show top-level listing
|
|
17
|
+
const registry = schema.getRegistry();
|
|
18
|
+
const entries = [];
|
|
19
|
+
|
|
20
|
+
for (const [name, entry] of Object.entries(registry)) {
|
|
21
|
+
entries.push({
|
|
22
|
+
Name: name,
|
|
23
|
+
Type: entry.type || 'unknown',
|
|
24
|
+
Description: entry.description || '',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (entries.length === 0) {
|
|
29
|
+
context.output.info('No commands found in registry.');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
context.output.out(entries);
|
|
34
|
+
context.output.info(`\nUse "dev schema <path>" to inspect a specific command.`);
|
|
35
|
+
context.output.info('Example: dev schema config.set');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = schema.resolve(dotPath);
|
|
40
|
+
|
|
41
|
+
if (!result) {
|
|
42
|
+
// Try to find suggestions by resolving the parent path
|
|
43
|
+
const parts = dotPath.split('.');
|
|
44
|
+
if (parts.length > 1) {
|
|
45
|
+
const parentPath = parts.slice(0, -1).join('.');
|
|
46
|
+
const parent = schema.resolve(parentPath);
|
|
47
|
+
if (parent) {
|
|
48
|
+
const children = parent.commands
|
|
49
|
+
? Object.keys(parent.commands)
|
|
50
|
+
: parent.resources
|
|
51
|
+
? Object.keys(parent.resources)
|
|
52
|
+
: [];
|
|
53
|
+
if (children.length > 0) {
|
|
54
|
+
context.output.error(`No command found at "${dotPath}".`);
|
|
55
|
+
context.output.info(`Available under "${parentPath}": ${children.join(', ')}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
context.output.error(`No command found at "${dotPath}".`);
|
|
62
|
+
context.output.info('Run "dev schema" to see all available paths.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Display the result
|
|
67
|
+
if (result.type === 'service') {
|
|
68
|
+
const cmds = Object.keys(result.commands || {});
|
|
69
|
+
if (context.flags.format === 'json') {
|
|
70
|
+
context.output.out(result);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
context.output.info(`Service: ${result.name}`);
|
|
74
|
+
context.output.info(`Description: ${result.description}`);
|
|
75
|
+
context.output.info('');
|
|
76
|
+
context.output.info(`Commands (${cmds.length}):`);
|
|
77
|
+
for (const cmdName of cmds) {
|
|
78
|
+
const cmd = result.commands[cmdName];
|
|
79
|
+
context.output.info(` ${cmdName.padEnd(20)} ${cmd.description || ''}`);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (result.type === 'plugin') {
|
|
85
|
+
const resources = Object.keys(result.resources || {});
|
|
86
|
+
if (context.flags.format === 'json') {
|
|
87
|
+
context.output.out(result);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
context.output.info(`Plugin: ${result.name} (v${result.version})`);
|
|
91
|
+
context.output.info(`Description: ${result.description}`);
|
|
92
|
+
context.output.info(`Auth: ${result.auth || 'none'}`);
|
|
93
|
+
context.output.info('');
|
|
94
|
+
context.output.info(`Resources (${resources.length}):`);
|
|
95
|
+
for (const resName of resources) {
|
|
96
|
+
const res = result.resources[resName];
|
|
97
|
+
context.output.info(` ${resName.padEnd(20)} ${res.description || ''}`);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (result.type === 'resource') {
|
|
103
|
+
const cmds = Object.keys(result.commands || {});
|
|
104
|
+
if (context.flags.format === 'json') {
|
|
105
|
+
context.output.out(result);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
context.output.info(`Resource: ${result.name}`);
|
|
109
|
+
context.output.info(`Description: ${result.description}`);
|
|
110
|
+
context.output.info('');
|
|
111
|
+
context.output.info(`Commands (${cmds.length}):`);
|
|
112
|
+
for (const cmdName of cmds) {
|
|
113
|
+
const cmd = result.commands[cmdName];
|
|
114
|
+
context.output.info(` ${cmdName.padEnd(20)} ${cmd.description || ''}`);
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (result.type === 'command') {
|
|
120
|
+
if (context.flags.format === 'json') {
|
|
121
|
+
context.output.out(result);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
context.output.info(`Command: ${result.name}`);
|
|
125
|
+
context.output.info(`Description: ${result.description}`);
|
|
126
|
+
|
|
127
|
+
const cmdArgs = result.arguments || [];
|
|
128
|
+
if (cmdArgs.length > 0) {
|
|
129
|
+
context.output.info('');
|
|
130
|
+
context.output.info('Arguments:');
|
|
131
|
+
for (const arg of cmdArgs) {
|
|
132
|
+
const req = arg.required ? '(required)' : '(optional)';
|
|
133
|
+
context.output.info(` ${(arg.name || '').padEnd(20)} ${req} ${arg.description || ''}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const cmdFlags = result.flags || [];
|
|
138
|
+
if (cmdFlags.length > 0) {
|
|
139
|
+
context.output.info('');
|
|
140
|
+
context.output.info('Flags:');
|
|
141
|
+
for (const flag of cmdFlags) {
|
|
142
|
+
context.output.info(` --${(flag.name || '').padEnd(18)} ${flag.description || ''}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback: just output the result
|
|
149
|
+
context.output.out(result);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const shell = require('../../lib/shell');
|
|
6
|
+
const { checkQmd } = require('./qmd');
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
description: 'Manage searchable collections (add, remove, list)',
|
|
10
|
+
arguments: [
|
|
11
|
+
{ name: 'action', description: 'Sub-command: add, remove, or list', required: true },
|
|
12
|
+
{ name: 'target', description: 'Directory path (for add) or collection name (for remove)', required: false }
|
|
13
|
+
],
|
|
14
|
+
flags: [
|
|
15
|
+
{ name: 'name', type: 'string', description: 'Collection name (required for add)' },
|
|
16
|
+
{ name: 'mask', type: 'string', description: 'Glob pattern to filter indexed files (e.g., "**/*.md")' },
|
|
17
|
+
{ name: 'confirm', type: 'boolean', description: 'Skip confirmation prompt (for remove)' }
|
|
18
|
+
]
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Manages QMD search collections: add, remove, or list.
|
|
23
|
+
* Each action shells out to the corresponding qmd collections command.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} args - Parsed CLI arguments { positional, flags }.
|
|
26
|
+
* @param {object} context - CLI context { output, errors, prompt }.
|
|
27
|
+
*/
|
|
28
|
+
async function run(args, context) {
|
|
29
|
+
// Check for QMD availability first
|
|
30
|
+
const qmd = checkQmd();
|
|
31
|
+
if (!qmd.available) {
|
|
32
|
+
context.errors.throwError(1, qmd.message, 'search');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const action = args.positional[0];
|
|
37
|
+
|
|
38
|
+
if (!action) {
|
|
39
|
+
context.output.info('Usage: dev search collections <add|remove|list>');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (action === 'list') {
|
|
44
|
+
// List all registered collections
|
|
45
|
+
const result = await shell.exec('qmd collections list');
|
|
46
|
+
|
|
47
|
+
if (result.exitCode !== 0) {
|
|
48
|
+
context.errors.throwError(1, result.stderr || 'Failed to list collections.', 'search');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Try to parse as JSON for structured output
|
|
53
|
+
const output = result.stdout;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(output);
|
|
56
|
+
context.output.out(parsed);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// Pass through as-is
|
|
59
|
+
context.output.info(output || 'No collections registered.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
} else if (action === 'add') {
|
|
63
|
+
// Add a new collection
|
|
64
|
+
const target = args.positional[1];
|
|
65
|
+
|
|
66
|
+
if (!target) {
|
|
67
|
+
context.errors.throwError(400, 'Missing directory path. Example: dev search collections add /path/to/dir --name docs', 'search');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const name = args.flags.name;
|
|
72
|
+
if (!name) {
|
|
73
|
+
context.errors.throwError(400, 'The --name flag is required when adding a collection.', 'search');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Resolve to an absolute path
|
|
78
|
+
const dirPath = path.resolve(target);
|
|
79
|
+
if (!fs.existsSync(dirPath)) {
|
|
80
|
+
context.errors.throwError(1, `Directory does not exist: ${dirPath}`, 'search');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build the qmd command
|
|
85
|
+
let cmd = `qmd collections add "${dirPath}" --name "${name}"`;
|
|
86
|
+
if (args.flags.mask) {
|
|
87
|
+
cmd += ` --mask "${args.flags.mask}"`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = await shell.exec(cmd);
|
|
91
|
+
|
|
92
|
+
if (result.exitCode !== 0) {
|
|
93
|
+
context.errors.throwError(1, result.stderr || 'Failed to add collection.', 'search');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
context.output.info(`Collection "${name}" added: ${dirPath}`);
|
|
98
|
+
|
|
99
|
+
} else if (action === 'remove') {
|
|
100
|
+
// Remove a collection
|
|
101
|
+
const name = args.positional[1];
|
|
102
|
+
|
|
103
|
+
if (!name) {
|
|
104
|
+
context.errors.throwError(400, 'Provide the collection name to remove. Example: dev search collections remove docs', 'search');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Ask for confirmation unless --confirm is passed
|
|
109
|
+
if (!args.flags.confirm) {
|
|
110
|
+
const ok = await context.prompt.confirm(
|
|
111
|
+
`Remove collection "${name}"?`,
|
|
112
|
+
false
|
|
113
|
+
);
|
|
114
|
+
if (!ok) {
|
|
115
|
+
context.output.info('Cancelled.');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result = await shell.exec(`qmd collections remove "${name}"`);
|
|
121
|
+
|
|
122
|
+
if (result.exitCode !== 0) {
|
|
123
|
+
context.errors.throwError(1, result.stderr || `Failed to remove collection "${name}".`, 'search');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
context.output.info(`Collection "${name}" removed.`);
|
|
128
|
+
|
|
129
|
+
} else {
|
|
130
|
+
context.output.info('Usage: dev search collections <add|remove|list>');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const shell = require('../../lib/shell');
|
|
4
|
+
const { checkQmd } = require('./qmd');
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
description: 'Retrieve a document by file path or QMD document ID',
|
|
8
|
+
arguments: [
|
|
9
|
+
{ name: 'target', description: 'File path or document ID (e.g., #abc123)', required: true }
|
|
10
|
+
],
|
|
11
|
+
flags: [
|
|
12
|
+
{ name: 'full', type: 'boolean', description: 'Return the entire document instead of a snippet' },
|
|
13
|
+
{ name: 'line', type: 'number', description: 'Start at a specific line number' },
|
|
14
|
+
{ name: 'max-lines', type: 'number', description: 'Maximum number of lines to return' }
|
|
15
|
+
]
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves a document by file path or QMD document ID.
|
|
20
|
+
* Passes the target through to QMD as-is (QMD handles both
|
|
21
|
+
* file paths and #id-style document IDs).
|
|
22
|
+
*
|
|
23
|
+
* @param {object} args - Parsed CLI arguments { positional, flags }.
|
|
24
|
+
* @param {object} context - CLI context { output, errors }.
|
|
25
|
+
*/
|
|
26
|
+
async function run(args, context) {
|
|
27
|
+
// Check for QMD availability first
|
|
28
|
+
const qmd = checkQmd();
|
|
29
|
+
if (!qmd.available) {
|
|
30
|
+
context.errors.throwError(1, qmd.message, 'search');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const target = args.positional[0];
|
|
35
|
+
|
|
36
|
+
if (!target) {
|
|
37
|
+
context.errors.throwError(400, 'Missing required argument: <target>. Example: dev search get path/to/document.md', 'search');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Build the QMD command
|
|
42
|
+
let cmd = `qmd get "${target}"`;
|
|
43
|
+
|
|
44
|
+
if (args.flags.full) {
|
|
45
|
+
cmd += ' --full';
|
|
46
|
+
}
|
|
47
|
+
if (args.flags.line) {
|
|
48
|
+
cmd += ` --line ${args.flags.line}`;
|
|
49
|
+
}
|
|
50
|
+
if (args.flags['max-lines']) {
|
|
51
|
+
cmd += ` --max-lines ${args.flags['max-lines']}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Execute and capture output
|
|
55
|
+
const result = await shell.exec(cmd);
|
|
56
|
+
|
|
57
|
+
if (result.exitCode !== 0) {
|
|
58
|
+
context.errors.throwError(1, result.stderr || `Document not found: ${target}`, 'search');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// For get, the output is document content, not search results
|
|
63
|
+
const output = {
|
|
64
|
+
target: target,
|
|
65
|
+
content: result.stdout
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
context.output.out(output);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const shell = require('../../lib/shell');
|
|
4
|
+
const { checkQmd } = require('./qmd');
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
description: 'Rebuild or update the search index and generate embeddings',
|
|
8
|
+
arguments: [],
|
|
9
|
+
flags: [
|
|
10
|
+
{ name: 'force', type: 'boolean', description: 'Re-index everything from scratch (ignores incremental updates)' }
|
|
11
|
+
]
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rebuilds or updates the QMD search index.
|
|
16
|
+
* Triggers re-indexing of all collections and embedding generation.
|
|
17
|
+
* Use --force to ignore incremental updates and rebuild from scratch.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} args - Parsed CLI arguments { positional, flags }.
|
|
20
|
+
* @param {object} context - CLI context { output, errors }.
|
|
21
|
+
*/
|
|
22
|
+
async function run(args, context) {
|
|
23
|
+
// Check for QMD availability first
|
|
24
|
+
const qmd = checkQmd();
|
|
25
|
+
if (!qmd.available) {
|
|
26
|
+
context.errors.throwError(1, qmd.message, 'search');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Build the command
|
|
31
|
+
let cmd = 'qmd index';
|
|
32
|
+
if (args.flags.force) {
|
|
33
|
+
cmd += ' --force';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Print progress messages before starting
|
|
37
|
+
context.output.info('Updating search index...');
|
|
38
|
+
if (args.flags.force) {
|
|
39
|
+
context.output.info('Force flag set -- re-indexing all documents from scratch.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Execute the index command
|
|
43
|
+
const result = await shell.exec(cmd);
|
|
44
|
+
|
|
45
|
+
if (result.exitCode !== 0) {
|
|
46
|
+
context.errors.throwError(1, result.stderr || 'Index update failed.', 'search');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pass through QMD's output (informational, not structured data)
|
|
51
|
+
context.output.info(result.stdout || 'Index updated successfully.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search service registration.
|
|
3
|
+
* Markdown search via QMD (requires separate install).
|
|
4
|
+
*
|
|
5
|
+
* All commands in this service check for QMD availability first.
|
|
6
|
+
* If QMD is not installed, they return an error with installation
|
|
7
|
+
* instructions: bun install -g @tobilu/qmd
|
|
8
|
+
*/
|
|
9
|
+
module.exports = {
|
|
10
|
+
name: 'search',
|
|
11
|
+
description: 'Markdown search via QMD',
|
|
12
|
+
commands: {
|
|
13
|
+
query: () => require('./query'),
|
|
14
|
+
keyword: () => require('./keyword'),
|
|
15
|
+
semantic: () => require('./semantic'),
|
|
16
|
+
get: () => require('./get'),
|
|
17
|
+
collections: () => require('./collections'),
|
|
18
|
+
index: () => require('./index-cmd'),
|
|
19
|
+
status: () => require('./status'),
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { checkQmd, runQmdSearch } = require('./qmd');
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
description: 'Fast BM25 full-text keyword search',
|
|
7
|
+
arguments: [
|
|
8
|
+
{ name: 'query', description: 'Search query text', required: true }
|
|
9
|
+
],
|
|
10
|
+
flags: [
|
|
11
|
+
{ name: 'collection', type: 'string', description: 'Restrict search to a specific collection' },
|
|
12
|
+
{ name: 'limit', type: 'number', description: 'Maximum number of results to return (default: 10)' }
|
|
13
|
+
]
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs a BM25 keyword search via QMD.
|
|
18
|
+
* Fast exact-term matching without vector similarity or LLM re-ranking.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} args - Parsed CLI arguments { positional, flags }.
|
|
21
|
+
* @param {object} context - CLI context { output, errors }.
|
|
22
|
+
*/
|
|
23
|
+
async function run(args, context) {
|
|
24
|
+
// Check for QMD availability first
|
|
25
|
+
const qmd = checkQmd();
|
|
26
|
+
if (!qmd.available) {
|
|
27
|
+
context.errors.throwError(1, qmd.message, 'search');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const query = args.positional[0];
|
|
32
|
+
|
|
33
|
+
if (!query) {
|
|
34
|
+
context.errors.throwError(400, 'Missing required argument: <query>. Example: dev search keyword "SSH key"', 'search');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build the QMD command
|
|
39
|
+
let cmd = `qmd keyword "${query}"`;
|
|
40
|
+
|
|
41
|
+
if (args.flags.collection) {
|
|
42
|
+
cmd += ` --collection "${args.flags.collection}"`;
|
|
43
|
+
}
|
|
44
|
+
if (args.flags.limit) {
|
|
45
|
+
cmd += ` --limit ${args.flags.limit}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Execute and parse results
|
|
49
|
+
const results = await runQmdSearch(cmd, context);
|
|
50
|
+
if (results === null) return; // error already reported
|
|
51
|
+
|
|
52
|
+
if (results.length === 0) {
|
|
53
|
+
context.output.info('No results found.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
context.output.out(results);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const shell = require('../../lib/shell');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks if QMD is installed and available on the system PATH.
|
|
7
|
+
* Every search command calls this before doing anything else.
|
|
8
|
+
*
|
|
9
|
+
* @returns {object} { available: true } if found, or { available: false, message: '...' } if not.
|
|
10
|
+
*/
|
|
11
|
+
function checkQmd() {
|
|
12
|
+
const isInstalled = shell.commandExists('qmd');
|
|
13
|
+
|
|
14
|
+
if (!isInstalled) {
|
|
15
|
+
return {
|
|
16
|
+
available: false,
|
|
17
|
+
message: 'QMD is not installed. Install it with: bun install -g @tobilu/qmd'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { available: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parses search results from QMD output.
|
|
26
|
+
* Tries JSON first, then falls back to line-delimited text.
|
|
27
|
+
* Returns an empty array for empty or null output.
|
|
28
|
+
*
|
|
29
|
+
* Output format may vary by QMD version.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} output - The raw stdout from a QMD search command.
|
|
32
|
+
* @returns {Array<object>} Parsed search results.
|
|
33
|
+
*/
|
|
34
|
+
function parseSearchResults(output) {
|
|
35
|
+
if (!output || !output.trim()) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Try JSON first
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(output);
|
|
42
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
43
|
+
} catch (err) {
|
|
44
|
+
// Fall back to line-delimited text
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse as line-delimited results
|
|
48
|
+
return output.trim().split('\n').filter(Boolean).map(line => ({ raw: line }));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Runs a QMD search command and parses the results.
|
|
53
|
+
* Returns null if the command fails (error is reported via context.errors).
|
|
54
|
+
*
|
|
55
|
+
* @param {string} cmd - The full QMD command to run.
|
|
56
|
+
* @param {object} context - The CLI context (needs context.errors).
|
|
57
|
+
* @returns {Promise<Array<object>|null>} Parsed results, or null on failure.
|
|
58
|
+
*/
|
|
59
|
+
async function runQmdSearch(cmd, context) {
|
|
60
|
+
const result = await shell.exec(cmd);
|
|
61
|
+
|
|
62
|
+
if (result.exitCode !== 0) {
|
|
63
|
+
context.errors.throwError(1, result.stderr || `QMD command failed: ${cmd}`, 'search');
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return parseSearchResults(result.stdout);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { checkQmd, parseSearchResults, runQmdSearch };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { checkQmd, runQmdSearch } = require('./qmd');
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
description: 'Hybrid search combining BM25 keyword matching, vector similarity, and LLM re-ranking',
|
|
7
|
+
arguments: [
|
|
8
|
+
{ name: 'query', description: 'Search query text', required: true }
|
|
9
|
+
],
|
|
10
|
+
flags: [
|
|
11
|
+
{ name: 'collection', type: 'string', description: 'Restrict search to a specific collection' },
|
|
12
|
+
{ name: 'limit', type: 'number', description: 'Maximum number of results to return (default: 10)' },
|
|
13
|
+
{ name: 'min-score', type: 'number', description: 'Minimum relevance score between 0.0 and 1.0' }
|
|
14
|
+
]
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runs a hybrid search (BM25 + vector + LLM re-ranking) via QMD.
|
|
19
|
+
* Provides the highest quality results at the cost of speed.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} args - Parsed CLI arguments { positional, flags }.
|
|
22
|
+
* @param {object} context - CLI context { output, errors }.
|
|
23
|
+
*/
|
|
24
|
+
async function run(args, context) {
|
|
25
|
+
// Check for QMD availability first
|
|
26
|
+
const qmd = checkQmd();
|
|
27
|
+
if (!qmd.available) {
|
|
28
|
+
context.errors.throwError(1, qmd.message, 'search');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const query = args.positional[0];
|
|
33
|
+
|
|
34
|
+
if (!query) {
|
|
35
|
+
context.errors.throwError(400, 'Missing required argument: <query>. Example: dev search query "how to configure SSH"', 'search');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Build the QMD command
|
|
40
|
+
let cmd = `qmd query "${query}"`;
|
|
41
|
+
|
|
42
|
+
if (args.flags.collection) {
|
|
43
|
+
cmd += ` --collection "${args.flags.collection}"`;
|
|
44
|
+
}
|
|
45
|
+
if (args.flags.limit) {
|
|
46
|
+
cmd += ` --limit ${args.flags.limit}`;
|
|
47
|
+
}
|
|
48
|
+
if (args.flags['min-score']) {
|
|
49
|
+
cmd += ` --min-score ${args.flags['min-score']}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Execute and parse results
|
|
53
|
+
const results = await runQmdSearch(cmd, context);
|
|
54
|
+
if (results === null) return; // error already reported
|
|
55
|
+
|
|
56
|
+
if (results.length === 0) {
|
|
57
|
+
context.output.info('No results found.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
context.output.out(results);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { meta, run };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { checkQmd, runQmdSearch } = require('./qmd');
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
description: 'Vector cosine similarity search for conceptually related content',
|
|
7
|
+
arguments: [
|
|
8
|
+
{ name: 'query', description: 'Search query text', required: true }
|
|
9
|
+
],
|
|
10
|
+
flags: [
|
|
11
|
+
{ name: 'collection', type: 'string', description: 'Restrict search to a specific collection' },
|
|
12
|
+
{ name: 'limit', type: 'number', description: 'Maximum number of results to return (default: 10)' }
|
|
13
|
+
]
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs a vector cosine similarity search via QMD.
|
|
18
|
+
* Finds conceptually similar content even when the exact words don't match.
|
|
19
|
+
* Requires embeddings to be generated first (via dev search index).
|
|
20
|
+
*
|
|
21
|
+
* @param {object} args - Parsed CLI arguments { positional, flags }.
|
|
22
|
+
* @param {object} context - CLI context { output, errors }.
|
|
23
|
+
*/
|
|
24
|
+
async function run(args, context) {
|
|
25
|
+
// Check for QMD availability first
|
|
26
|
+
const qmd = checkQmd();
|
|
27
|
+
if (!qmd.available) {
|
|
28
|
+
context.errors.throwError(1, qmd.message, 'search');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const query = args.positional[0];
|
|
33
|
+
|
|
34
|
+
if (!query) {
|
|
35
|
+
context.errors.throwError(400, 'Missing required argument: <query>. Example: dev search semantic "remote server access"', 'search');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Build the QMD command
|
|
40
|
+
let cmd = `qmd semantic "${query}"`;
|
|
41
|
+
|
|
42
|
+
if (args.flags.collection) {
|
|
43
|
+
cmd += ` --collection "${args.flags.collection}"`;
|
|
44
|
+
}
|
|
45
|
+
if (args.flags.limit) {
|
|
46
|
+
cmd += ` --limit ${args.flags.limit}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Execute and parse results
|
|
50
|
+
const results = await runQmdSearch(cmd, context);
|
|
51
|
+
if (results === null) return; // error already reported
|
|
52
|
+
|
|
53
|
+
if (results.length === 0) {
|
|
54
|
+
// Check if this might be an embeddings issue
|
|
55
|
+
context.output.info('No results found. If embeddings have not been generated, run "dev search index" first.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
context.output.out(results);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { meta, run };
|