start-command 0.3.1

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 (38) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.changeset/isolation-support.md +30 -0
  4. package/.github/workflows/release.yml +292 -0
  5. package/.husky/pre-commit +1 -0
  6. package/.prettierignore +6 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +24 -0
  9. package/LICENSE +24 -0
  10. package/README.md +249 -0
  11. package/REQUIREMENTS.md +229 -0
  12. package/bun.lock +453 -0
  13. package/bunfig.toml +3 -0
  14. package/eslint.config.mjs +122 -0
  15. package/experiments/debug-regex.js +49 -0
  16. package/experiments/isolation-design.md +142 -0
  17. package/experiments/test-cli.sh +42 -0
  18. package/experiments/test-substitution.js +143 -0
  19. package/package.json +63 -0
  20. package/scripts/changeset-version.mjs +38 -0
  21. package/scripts/check-file-size.mjs +103 -0
  22. package/scripts/create-github-release.mjs +93 -0
  23. package/scripts/create-manual-changeset.mjs +89 -0
  24. package/scripts/format-github-release.mjs +83 -0
  25. package/scripts/format-release-notes.mjs +219 -0
  26. package/scripts/instant-version-bump.mjs +121 -0
  27. package/scripts/publish-to-npm.mjs +129 -0
  28. package/scripts/setup-npm.mjs +37 -0
  29. package/scripts/validate-changeset.mjs +107 -0
  30. package/scripts/version-and-commit.mjs +237 -0
  31. package/src/bin/cli.js +670 -0
  32. package/src/lib/args-parser.js +259 -0
  33. package/src/lib/isolation.js +419 -0
  34. package/src/lib/substitution.js +323 -0
  35. package/src/lib/substitutions.lino +308 -0
  36. package/test/args-parser.test.js +389 -0
  37. package/test/isolation.test.js +248 -0
  38. package/test/substitution.test.js +236 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Substitution Engine for start-command
3
+ * Parses .lino files and matches natural language commands to shell commands
4
+ *
5
+ * Uses Links Notation style patterns with variables like $packageName, $version
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ // Debug mode from environment
12
+ const DEBUG =
13
+ process.env.START_DEBUG === '1' || process.env.START_DEBUG === 'true';
14
+
15
+ /**
16
+ * Parse a .lino substitutions file
17
+ * @param {string} filePath - Path to the .lino file
18
+ * @returns {Array<{pattern: string, replacement: string, regex: RegExp, variables: string[]}>}
19
+ */
20
+ function parseLinoFile(filePath) {
21
+ const content = fs.readFileSync(filePath, 'utf8');
22
+ return parseLinoContent(content);
23
+ }
24
+
25
+ /**
26
+ * Parse .lino content string
27
+ * @param {string} content - Content of the .lino file
28
+ * @returns {Array<{pattern: string, replacement: string, regex: RegExp, variables: string[]}>}
29
+ */
30
+ function parseLinoContent(content) {
31
+ const rules = [];
32
+ const lines = content.split('\n');
33
+
34
+ let i = 0;
35
+ while (i < lines.length) {
36
+ const line = lines[i].trim();
37
+
38
+ // Skip empty lines and comments
39
+ if (!line || line.startsWith('#')) {
40
+ i++;
41
+ continue;
42
+ }
43
+
44
+ // Look for opening parenthesis of doublet link
45
+ if (line === '(') {
46
+ i++;
47
+
48
+ // Find the pattern line (first non-empty, non-comment line)
49
+ let pattern = null;
50
+ while (i < lines.length) {
51
+ const patternLine = lines[i].trim();
52
+ if (
53
+ patternLine &&
54
+ !patternLine.startsWith('#') &&
55
+ patternLine !== ')'
56
+ ) {
57
+ pattern = patternLine;
58
+ i++;
59
+ break;
60
+ }
61
+ i++;
62
+ }
63
+
64
+ // Find the replacement line (second non-empty, non-comment line)
65
+ let replacement = null;
66
+ while (i < lines.length) {
67
+ const replacementLine = lines[i].trim();
68
+ if (
69
+ replacementLine &&
70
+ !replacementLine.startsWith('#') &&
71
+ replacementLine !== ')'
72
+ ) {
73
+ replacement = replacementLine;
74
+ i++;
75
+ break;
76
+ }
77
+ i++;
78
+ }
79
+
80
+ // Find closing parenthesis
81
+ while (i < lines.length) {
82
+ const closeLine = lines[i].trim();
83
+ if (closeLine === ')') {
84
+ break;
85
+ }
86
+ i++;
87
+ }
88
+
89
+ // Create rule if both pattern and replacement found
90
+ if (pattern && replacement) {
91
+ const rule = createRule(pattern, replacement);
92
+ if (rule) {
93
+ rules.push(rule);
94
+ }
95
+ }
96
+ }
97
+
98
+ i++;
99
+ }
100
+
101
+ return rules;
102
+ }
103
+
104
+ /**
105
+ * Create a rule object from pattern and replacement strings
106
+ * @param {string} pattern - The matching pattern with variables
107
+ * @param {string} replacement - The replacement pattern
108
+ * @returns {{pattern: string, replacement: string, regex: RegExp, variables: string[]}|null}
109
+ */
110
+ function createRule(pattern, replacement) {
111
+ // Extract variables from pattern (words starting with $)
112
+ const variables = [];
113
+ const variablePattern = /\$(\w+)/g;
114
+ let match;
115
+
116
+ while ((match = variablePattern.exec(pattern)) !== null) {
117
+ variables.push(match[1]);
118
+ }
119
+
120
+ // Convert pattern to regex
121
+ // First, replace $variables with placeholders
122
+ let tempPattern = pattern;
123
+ const placeholders = [];
124
+
125
+ for (let i = 0; i < variables.length; i++) {
126
+ const varName = variables[i];
127
+ const placeholder = `__VAR_${i}__`;
128
+ placeholders.push({ placeholder, varName });
129
+ // Replace first occurrence of this variable
130
+ tempPattern = tempPattern.replace(`$${varName}`, placeholder);
131
+ }
132
+
133
+ // Escape special regex characters in the remaining text
134
+ let regexStr = tempPattern.replace(/[.*+?^{}()|[\]\\]/g, '\\$&');
135
+
136
+ // Replace placeholders with named capture groups
137
+ // Use .+? for greedy-enough matching but not too greedy
138
+ for (const { placeholder, varName } of placeholders) {
139
+ regexStr = regexStr.replace(placeholder, `(?<${varName}>.+?)`);
140
+ }
141
+
142
+ // Make the regex match the entire string with optional whitespace
143
+ regexStr = `^\\s*${regexStr}\\s*$`;
144
+
145
+ try {
146
+ const regex = new RegExp(regexStr, 'i'); // Case insensitive
147
+ return { pattern, replacement, regex, variables };
148
+ } catch (err) {
149
+ if (DEBUG) {
150
+ console.error(`Invalid pattern: ${pattern} - ${err.message}`);
151
+ }
152
+ return null;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Sort rules so more specific patterns (more variables, longer patterns) match first
158
+ * @param {Array} rules - Array of rule objects
159
+ * @returns {Array} Sorted rules
160
+ */
161
+ function sortRulesBySpecificity(rules) {
162
+ return [...rules].sort((a, b) => {
163
+ // More variables = more specific, should come first
164
+ if (b.variables.length !== a.variables.length) {
165
+ return b.variables.length - a.variables.length;
166
+ }
167
+ // Longer patterns = more specific
168
+ return b.pattern.length - a.pattern.length;
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Match input against rules and return the substituted command
174
+ * @param {string} input - The user input command
175
+ * @param {Array} rules - Array of rule objects
176
+ * @returns {{matched: boolean, original: string, command: string, rule: object|null}}
177
+ */
178
+ function matchAndSubstitute(input, rules) {
179
+ const trimmedInput = input.trim();
180
+
181
+ // Sort rules by specificity (more specific patterns first)
182
+ const sortedRules = sortRulesBySpecificity(rules);
183
+
184
+ for (const rule of sortedRules) {
185
+ const match = trimmedInput.match(rule.regex);
186
+
187
+ if (match) {
188
+ // Build the substituted command
189
+ let command = rule.replacement;
190
+
191
+ // Replace variables with captured values
192
+ for (const varName of rule.variables) {
193
+ const value = match.groups[varName];
194
+ if (value !== undefined) {
195
+ command = command.replace(new RegExp(`\\$${varName}`, 'g'), value);
196
+ }
197
+ }
198
+
199
+ return {
200
+ matched: true,
201
+ original: input,
202
+ command,
203
+ rule,
204
+ };
205
+ }
206
+ }
207
+
208
+ // No match found - return original input
209
+ return {
210
+ matched: false,
211
+ original: input,
212
+ command: input,
213
+ rule: null,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Load default substitutions from the package's substitutions.lino file
219
+ * @returns {Array} Array of rules
220
+ */
221
+ function loadDefaultSubstitutions() {
222
+ // Look for substitutions.lino in the same directory as this file (src/lib)
223
+ // __dirname is src/lib, substitutions.lino is in the same directory
224
+ const defaultLinoPath = path.join(__dirname, 'substitutions.lino');
225
+
226
+ if (fs.existsSync(defaultLinoPath)) {
227
+ try {
228
+ return parseLinoFile(defaultLinoPath);
229
+ } catch (err) {
230
+ if (DEBUG) {
231
+ console.error(`Failed to load default substitutions: ${err.message}`);
232
+ }
233
+ return [];
234
+ }
235
+ }
236
+
237
+ return [];
238
+ }
239
+
240
+ /**
241
+ * Load user substitutions from custom path or home directory
242
+ * @param {string} customPath - Optional custom path to .lino file
243
+ * @returns {Array} Array of rules
244
+ */
245
+ function loadUserSubstitutions(customPath) {
246
+ // If custom path provided, use it
247
+ if (customPath && fs.existsSync(customPath)) {
248
+ try {
249
+ return parseLinoFile(customPath);
250
+ } catch (err) {
251
+ if (DEBUG) {
252
+ console.error(`Failed to load user substitutions: ${err.message}`);
253
+ }
254
+ return [];
255
+ }
256
+ }
257
+
258
+ // Look in home directory for .start-command/substitutions.lino
259
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
260
+ if (homeDir) {
261
+ const userLinoPath = path.join(
262
+ homeDir,
263
+ '.start-command',
264
+ 'substitutions.lino'
265
+ );
266
+ if (fs.existsSync(userLinoPath)) {
267
+ try {
268
+ return parseLinoFile(userLinoPath);
269
+ } catch (err) {
270
+ if (DEBUG) {
271
+ console.error(`Failed to load user substitutions: ${err.message}`);
272
+ }
273
+ }
274
+ }
275
+ }
276
+
277
+ return [];
278
+ }
279
+
280
+ /**
281
+ * Process a command through the substitution engine
282
+ * @param {string} input - The input command
283
+ * @param {object} options - Options { customLinoPath, verbose }
284
+ * @returns {{matched: boolean, original: string, command: string, rule: object|null}}
285
+ */
286
+ function processCommand(input, options = {}) {
287
+ const { customLinoPath, verbose } = options;
288
+
289
+ // Load rules: user rules take precedence
290
+ const userRules = loadUserSubstitutions(customLinoPath);
291
+ const defaultRules = loadDefaultSubstitutions();
292
+
293
+ // User rules first, then default rules
294
+ const allRules = [...userRules, ...defaultRules];
295
+
296
+ if (allRules.length === 0) {
297
+ return {
298
+ matched: false,
299
+ original: input,
300
+ command: input,
301
+ rule: null,
302
+ };
303
+ }
304
+
305
+ const result = matchAndSubstitute(input, allRules);
306
+
307
+ if (verbose && result.matched) {
308
+ console.log(`Pattern matched: "${result.rule.pattern}"`);
309
+ console.log(`Translated to: ${result.command}`);
310
+ }
311
+
312
+ return result;
313
+ }
314
+
315
+ module.exports = {
316
+ parseLinoFile,
317
+ parseLinoContent,
318
+ createRule,
319
+ matchAndSubstitute,
320
+ loadDefaultSubstitutions,
321
+ loadUserSubstitutions,
322
+ processCommand,
323
+ };
@@ -0,0 +1,308 @@
1
+ # Substitutions file for start-command
2
+ # Format: Each rule is a doublet link (pattern, replacement)
3
+ # Variables: $packageName, $version, $repository, $url, $path, $branch, etc.
4
+ #
5
+ # This file uses Links Notation style patterns for matching natural language commands
6
+ # and translating them to actual shell commands.
7
+
8
+ # === NPM Package Installation ===
9
+
10
+ # install {packageName} npm package
11
+ (
12
+ install $packageName npm package
13
+ npm install $packageName
14
+ )
15
+
16
+ # install {version} version of {packageName} npm package
17
+ (
18
+ install $version version of $packageName npm package
19
+ npm install $packageName@$version
20
+ )
21
+
22
+ # install {packageName} npm package globally
23
+ (
24
+ install $packageName npm package globally
25
+ npm install -g $packageName
26
+ )
27
+
28
+ # install {packageName} globally
29
+ (
30
+ install $packageName globally
31
+ npm install -g $packageName
32
+ )
33
+
34
+ # install {version} version of {packageName} npm package globally
35
+ (
36
+ install $version version of $packageName npm package globally
37
+ npm install -g $packageName@$version
38
+ )
39
+
40
+ # uninstall {packageName} npm package
41
+ (
42
+ uninstall $packageName npm package
43
+ npm uninstall $packageName
44
+ )
45
+
46
+ # uninstall {packageName} npm package globally
47
+ (
48
+ uninstall $packageName npm package globally
49
+ npm uninstall -g $packageName
50
+ )
51
+
52
+ # remove {packageName} npm package
53
+ (
54
+ remove $packageName npm package
55
+ npm uninstall $packageName
56
+ )
57
+
58
+ # update {packageName} npm package
59
+ (
60
+ update $packageName npm package
61
+ npm update $packageName
62
+ )
63
+
64
+ # === Git Repository Operations ===
65
+
66
+ # clone {repository} repository
67
+ (
68
+ clone $repository repository
69
+ git clone $repository
70
+ )
71
+
72
+ # clone {url}
73
+ (
74
+ clone $url
75
+ git clone $url
76
+ )
77
+
78
+ # clone {repository} repository to {path}
79
+ (
80
+ clone $repository repository to $path
81
+ git clone $repository $path
82
+ )
83
+
84
+ # clone {repository} repository into {path}
85
+ (
86
+ clone $repository repository into $path
87
+ git clone $repository $path
88
+ )
89
+
90
+ # checkout {branch} branch
91
+ (
92
+ checkout $branch branch
93
+ git checkout $branch
94
+ )
95
+
96
+ # create {branch} branch
97
+ (
98
+ create $branch branch
99
+ git checkout -b $branch
100
+ )
101
+
102
+ # switch to {branch} branch
103
+ (
104
+ switch to $branch branch
105
+ git checkout $branch
106
+ )
107
+
108
+ # push to {remote}
109
+ (
110
+ push to $remote
111
+ git push $remote
112
+ )
113
+
114
+ # push to {remote} {branch}
115
+ (
116
+ push to $remote $branch
117
+ git push $remote $branch
118
+ )
119
+
120
+ # pull from {remote}
121
+ (
122
+ pull from $remote
123
+ git pull $remote
124
+ )
125
+
126
+ # pull from {remote} {branch}
127
+ (
128
+ pull from $remote $branch
129
+ git pull $remote $branch
130
+ )
131
+
132
+ # === Common Operations ===
133
+
134
+ # list files
135
+ (
136
+ list files
137
+ ls -la
138
+ )
139
+
140
+ # list files in {path}
141
+ (
142
+ list files in $path
143
+ ls -la $path
144
+ )
145
+
146
+ # show current directory
147
+ (
148
+ show current directory
149
+ pwd
150
+ )
151
+
152
+ # show working directory
153
+ (
154
+ show working directory
155
+ pwd
156
+ )
157
+
158
+ # create {name} directory
159
+ (
160
+ create $name directory
161
+ mkdir -p $name
162
+ )
163
+
164
+ # create directory {name}
165
+ (
166
+ create directory $name
167
+ mkdir -p $name
168
+ )
169
+
170
+ # remove {name} directory
171
+ (
172
+ remove $name directory
173
+ rm -rf $name
174
+ )
175
+
176
+ # delete {name} directory
177
+ (
178
+ delete $name directory
179
+ rm -rf $name
180
+ )
181
+
182
+ # remove {name} file
183
+ (
184
+ remove $name file
185
+ rm $name
186
+ )
187
+
188
+ # delete {name} file
189
+ (
190
+ delete $name file
191
+ rm $name
192
+ )
193
+
194
+ # show {file} contents
195
+ (
196
+ show $file contents
197
+ cat $file
198
+ )
199
+
200
+ # read {file}
201
+ (
202
+ read $file
203
+ cat $file
204
+ )
205
+
206
+ # === Process/System Operations ===
207
+
208
+ # show running processes
209
+ (
210
+ show running processes
211
+ ps aux
212
+ )
213
+
214
+ # find process {name}
215
+ (
216
+ find process $name
217
+ ps aux | grep $name
218
+ )
219
+
220
+ # kill process {pid}
221
+ (
222
+ kill process $pid
223
+ kill $pid
224
+ )
225
+
226
+ # === Docker Operations ===
227
+
228
+ # list docker containers
229
+ (
230
+ list docker containers
231
+ docker ps
232
+ )
233
+
234
+ # list all docker containers
235
+ (
236
+ list all docker containers
237
+ docker ps -a
238
+ )
239
+
240
+ # start {container} container
241
+ (
242
+ start $container container
243
+ docker start $container
244
+ )
245
+
246
+ # stop {container} container
247
+ (
248
+ stop $container container
249
+ docker stop $container
250
+ )
251
+
252
+ # remove {container} container
253
+ (
254
+ remove $container container
255
+ docker rm $container
256
+ )
257
+
258
+ # build docker image from {path}
259
+ (
260
+ build docker image from $path
261
+ docker build $path
262
+ )
263
+
264
+ # build docker image {name} from {path}
265
+ (
266
+ build docker image $name from $path
267
+ docker build -t $name $path
268
+ )
269
+
270
+ # === Python/Pip Operations ===
271
+
272
+ # install {packageName} python package
273
+ (
274
+ install $packageName python package
275
+ pip install $packageName
276
+ )
277
+
278
+ # install {packageName} pip package
279
+ (
280
+ install $packageName pip package
281
+ pip install $packageName
282
+ )
283
+
284
+ # install {version} version of {packageName} python package
285
+ (
286
+ install $version version of $packageName python package
287
+ pip install $packageName==$version
288
+ )
289
+
290
+ # uninstall {packageName} python package
291
+ (
292
+ uninstall $packageName python package
293
+ pip uninstall $packageName
294
+ )
295
+
296
+ # === Help Patterns ===
297
+
298
+ # show help
299
+ (
300
+ show help
301
+ $ --help
302
+ )
303
+
304
+ # help
305
+ (
306
+ help
307
+ $ --help
308
+ )