@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.
Files changed (122) hide show
  1. package/README.md +223 -32
  2. package/package.json +7 -5
  3. package/src/api/loader.js +229 -0
  4. package/src/api/registry.json +62 -0
  5. package/src/cli.js +305 -0
  6. package/src/commands/ai/index.js +16 -0
  7. package/src/commands/ai/launch.js +112 -0
  8. package/src/commands/ai/list.js +54 -0
  9. package/src/commands/ai/resume.js +70 -0
  10. package/src/commands/ai/sessions.js +121 -0
  11. package/src/commands/ai/set.js +131 -0
  12. package/src/commands/ai/show.js +74 -0
  13. package/src/commands/ai/tools.js +46 -0
  14. package/src/commands/alias/add.js +93 -0
  15. package/src/commands/alias/helpers.js +107 -0
  16. package/src/commands/alias/index.js +14 -0
  17. package/src/commands/alias/list.js +55 -0
  18. package/src/commands/alias/remove.js +62 -0
  19. package/src/commands/alias/sync.js +109 -0
  20. package/src/commands/api/disable.js +73 -0
  21. package/src/commands/api/enable.js +148 -0
  22. package/src/commands/api/index.js +15 -0
  23. package/src/commands/api/list.js +66 -0
  24. package/src/commands/api/update.js +87 -0
  25. package/src/commands/auth/index.js +15 -0
  26. package/src/commands/auth/list.js +49 -0
  27. package/src/commands/auth/login.js +384 -0
  28. package/src/commands/auth/logout.js +111 -0
  29. package/src/commands/auth/refresh.js +184 -0
  30. package/src/commands/auth/services.js +169 -0
  31. package/src/commands/auth/status.js +104 -0
  32. package/src/commands/config/export.js +224 -0
  33. package/src/commands/config/get.js +52 -0
  34. package/src/commands/config/import.js +308 -0
  35. package/src/commands/config/index.js +17 -0
  36. package/src/commands/config/init.js +143 -0
  37. package/src/commands/config/reset.js +57 -0
  38. package/src/commands/config/set.js +93 -0
  39. package/src/commands/config/show.js +35 -0
  40. package/src/commands/help.js +338 -0
  41. package/src/commands/identity/add.js +133 -0
  42. package/src/commands/identity/index.js +17 -0
  43. package/src/commands/identity/link.js +76 -0
  44. package/src/commands/identity/list.js +48 -0
  45. package/src/commands/identity/remove.js +72 -0
  46. package/src/commands/identity/show.js +65 -0
  47. package/src/commands/identity/sync.js +172 -0
  48. package/src/commands/identity/unlink.js +57 -0
  49. package/src/commands/ignore/add.js +165 -0
  50. package/src/commands/ignore/index.js +14 -0
  51. package/src/commands/ignore/list.js +89 -0
  52. package/src/commands/ignore/markers.js +43 -0
  53. package/src/commands/ignore/remove.js +164 -0
  54. package/src/commands/ignore/show.js +169 -0
  55. package/src/commands/machine/detect.js +122 -0
  56. package/src/commands/machine/index.js +14 -0
  57. package/src/commands/machine/list.js +74 -0
  58. package/src/commands/machine/set.js +106 -0
  59. package/src/commands/machine/show.js +35 -0
  60. package/src/commands/schema.js +152 -0
  61. package/src/commands/search/collections.js +134 -0
  62. package/src/commands/search/get.js +71 -0
  63. package/src/commands/search/index-cmd.js +54 -0
  64. package/src/commands/search/index.js +21 -0
  65. package/src/commands/search/keyword.js +60 -0
  66. package/src/commands/search/qmd.js +70 -0
  67. package/src/commands/search/query.js +64 -0
  68. package/src/commands/search/semantic.js +62 -0
  69. package/src/commands/search/status.js +46 -0
  70. package/src/commands/status.js +276 -0
  71. package/src/commands/tools/check.js +79 -0
  72. package/src/commands/tools/index.js +14 -0
  73. package/src/commands/tools/install.js +110 -0
  74. package/src/commands/tools/list.js +91 -0
  75. package/src/commands/tools/search.js +60 -0
  76. package/src/commands/update.js +113 -0
  77. package/src/commands/util/add.js +151 -0
  78. package/src/commands/util/index.js +15 -0
  79. package/src/commands/util/list.js +97 -0
  80. package/src/commands/util/remove.js +76 -0
  81. package/src/commands/util/run.js +79 -0
  82. package/src/commands/util/show.js +67 -0
  83. package/src/commands/version.js +33 -0
  84. package/src/installers/_template.js +104 -0
  85. package/src/installers/git.js +150 -0
  86. package/src/installers/homebrew.js +190 -0
  87. package/src/installers/node.js +223 -0
  88. package/src/installers/registry.json +29 -0
  89. package/src/lib/config.js +125 -0
  90. package/src/lib/detect.js +74 -0
  91. package/src/lib/errors.js +114 -0
  92. package/src/lib/github.js +315 -0
  93. package/src/lib/installer.js +225 -0
  94. package/src/lib/output.js +239 -0
  95. package/src/lib/platform.js +112 -0
  96. package/src/lib/platforms/amazon-linux.js +41 -0
  97. package/src/lib/platforms/gitbash.js +46 -0
  98. package/src/lib/platforms/macos.js +45 -0
  99. package/src/lib/platforms/raspbian.js +41 -0
  100. package/src/lib/platforms/ubuntu.js +39 -0
  101. package/src/lib/platforms/windows.js +45 -0
  102. package/src/lib/prompt.js +161 -0
  103. package/src/lib/schema.js +211 -0
  104. package/src/lib/shell.js +75 -0
  105. package/src/patterns/gitignore/claude-code.txt +25 -0
  106. package/src/patterns/gitignore/docker.txt +15 -0
  107. package/src/patterns/gitignore/go.txt +24 -0
  108. package/src/patterns/gitignore/java.txt +38 -0
  109. package/src/patterns/gitignore/jetbrains.txt +26 -0
  110. package/src/patterns/gitignore/linux.txt +18 -0
  111. package/src/patterns/gitignore/macos.txt +27 -0
  112. package/src/patterns/gitignore/node.txt +51 -0
  113. package/src/patterns/gitignore/python.txt +55 -0
  114. package/src/patterns/gitignore/rust.txt +14 -0
  115. package/src/patterns/gitignore/terraform.txt +30 -0
  116. package/src/patterns/gitignore/vscode.txt +15 -0
  117. package/src/patterns/gitignore/windows.txt +25 -0
  118. package/src/utils/clone/index.js +165 -0
  119. package/src/utils/git-push/index.js +230 -0
  120. package/src/utils/git-status/index.js +116 -0
  121. package/src/utils/git-status/unix.sh +75 -0
  122. 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 };