@impulselab/directory 1.0.8 → 2.7.4
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 +60 -120
- package/dist/index.js +1595 -0
- package/package.json +28 -20
- package/index.js +0 -666
package/index.js
DELETED
|
@@ -1,666 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const https = require('https');
|
|
6
|
-
const http = require('http');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
|
|
9
|
-
const PACKAGE = {
|
|
10
|
-
scopeName: '@impulselab/directory',
|
|
11
|
-
commandName: 'impulse-directory',
|
|
12
|
-
aliasName: 'directory',
|
|
13
|
-
brand: 'Impulse Lab',
|
|
14
|
-
websiteUrl: 'https://impulselab.ai',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const DEFAULT_BASE_URL = 'impulse.directory';
|
|
18
|
-
const RAW_PATH = '/raw/';
|
|
19
|
-
const STACK_API_PREFIX = '/api/stacks/';
|
|
20
|
-
const STACK_API_SUFFIX = '/npx';
|
|
21
|
-
|
|
22
|
-
function normalizeBaseHost(input) {
|
|
23
|
-
const raw = (input || '').trim();
|
|
24
|
-
if (!raw) return '';
|
|
25
|
-
let host = raw.replace(/^\s*https?:\/\//i, '');
|
|
26
|
-
host = host.replace(/\/+$/, '');
|
|
27
|
-
return host;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function isLocalHost(host) {
|
|
31
|
-
const hostnameOnly = (host || '').split(':')[0];
|
|
32
|
-
return (
|
|
33
|
-
hostnameOnly === 'localhost' ||
|
|
34
|
-
hostnameOnly === '127.0.0.1' ||
|
|
35
|
-
hostnameOnly === '::1'
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function canFallbackToHttp(host) {
|
|
40
|
-
return process.env.ALLOW_HTTP_FALLBACK === '1' || isLocalHost(host);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const MODES = {
|
|
44
|
-
COMMAND: 'command',
|
|
45
|
-
STACK: 'stack',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const INSTALLATION_TARGETS = {
|
|
49
|
-
CLAUDE_SLASH: 'claude-slash',
|
|
50
|
-
CLAUDE_SUB_AGENT: 'claude-sub-agent',
|
|
51
|
-
CLAUDE_SKILL: 'claude-skill',
|
|
52
|
-
CURSOR_COMMAND: 'cursor-command',
|
|
53
|
-
CURSOR_RULE: 'cursor-rule',
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const DEFAULT_TARGET = INSTALLATION_TARGETS.CLAUDE_SLASH;
|
|
57
|
-
|
|
58
|
-
const TARGET_CONFIG = {
|
|
59
|
-
[INSTALLATION_TARGETS.CLAUDE_SLASH]: {
|
|
60
|
-
label: 'Claude slash command',
|
|
61
|
-
description: '.claude/commands (falls back to ~/.claude/commands)',
|
|
62
|
-
resolveDirectory: () => resolveClaudeDirectory('commands'),
|
|
63
|
-
extension: '.md',
|
|
64
|
-
},
|
|
65
|
-
[INSTALLATION_TARGETS.CLAUDE_SUB_AGENT]: {
|
|
66
|
-
label: 'Claude sub agent',
|
|
67
|
-
description: '.claude/agents (falls back to ~/.claude/agents)',
|
|
68
|
-
resolveDirectory: () => resolveClaudeDirectory('agents'),
|
|
69
|
-
extension: '.md',
|
|
70
|
-
},
|
|
71
|
-
[INSTALLATION_TARGETS.CLAUDE_SKILL]: {
|
|
72
|
-
label: 'Claude Code skill',
|
|
73
|
-
description: '.claude/skills (project only)',
|
|
74
|
-
resolveDirectory: () => resolveClaudeSkillsDirectory(),
|
|
75
|
-
extension: '.md',
|
|
76
|
-
isSkill: true,
|
|
77
|
-
},
|
|
78
|
-
[INSTALLATION_TARGETS.CURSOR_COMMAND]: {
|
|
79
|
-
label: 'Cursor command',
|
|
80
|
-
description: '.cursor/commands (project only)',
|
|
81
|
-
resolveDirectory: () => resolveCursorDirectory('commands'),
|
|
82
|
-
extension: '.md',
|
|
83
|
-
},
|
|
84
|
-
[INSTALLATION_TARGETS.CURSOR_RULE]: {
|
|
85
|
-
label: 'Cursor rule',
|
|
86
|
-
description: '.cursor/rules (project only)',
|
|
87
|
-
resolveDirectory: () => resolveCursorDirectory('rules'),
|
|
88
|
-
extension: '.mdc',
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const KNOWN_TARGETS = Object.keys(TARGET_CONFIG);
|
|
93
|
-
|
|
94
|
-
function showHelp() {
|
|
95
|
-
const primaryUsageCmd = `npx impulse-directory`;
|
|
96
|
-
console.log(`
|
|
97
|
-
Impulse Directory Command Installer
|
|
98
|
-
Made by ${PACKAGE.brand} (${PACKAGE.websiteUrl})
|
|
99
|
-
|
|
100
|
-
Usage:
|
|
101
|
-
${primaryUsageCmd} <user-slug>/<command-slug> [--target <target>]
|
|
102
|
-
${primaryUsageCmd} stack <stack-id>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Description:
|
|
106
|
-
Downloads a command from Impulse Directory and installs it in the right tool folder.
|
|
107
|
-
When using the stack mode, installs each prompt in the stack to its inferred target.
|
|
108
|
-
|
|
109
|
-
Arguments:
|
|
110
|
-
user-slug/command-slug The full path to the command (user/command)
|
|
111
|
-
stack-id The stack ULID shown in the Impulse Directory UI
|
|
112
|
-
|
|
113
|
-
Options:
|
|
114
|
-
--target <target> Installation target (optional for single commands)
|
|
115
|
-
-t <target> Short alias for --target
|
|
116
|
-
--help, -h Show this help text
|
|
117
|
-
|
|
118
|
-
Targets:
|
|
119
|
-
${KNOWN_TARGETS.map((key) => {
|
|
120
|
-
const config = TARGET_CONFIG[key];
|
|
121
|
-
const defaultLabel = key === DEFAULT_TARGET ? ' (default)' : '';
|
|
122
|
-
return ` ${key.padEnd(18)} ${config.label}${defaultLabel}\n -> ${config.description}`;
|
|
123
|
-
}).join('\n')}
|
|
124
|
-
|
|
125
|
-
Environment Variables:
|
|
126
|
-
IMPULSE_BASE_URL Base host for Impulse Directory (hostname[:port], no scheme, no trailing slash)
|
|
127
|
-
Examples: impulse.directory | localhost:3000
|
|
128
|
-
|
|
129
|
-
Examples:
|
|
130
|
-
${primaryUsageCmd} john/my-awesome-command
|
|
131
|
-
${primaryUsageCmd} john/my-awesome-command --target claude-sub-agent
|
|
132
|
-
${primaryUsageCmd} john/my-awesome-command --target cursor-command
|
|
133
|
-
${primaryUsageCmd} stack 01JH0000000ABCDEF --target claude-slash
|
|
134
|
-
`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function shouldUseHttps(baseUrl) {
|
|
138
|
-
if (baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1')) {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function toKebabCase(input) {
|
|
145
|
-
return String(input)
|
|
146
|
-
.normalize('NFKD') // strip accents
|
|
147
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
148
|
-
.replace(/['"]/g, '')
|
|
149
|
-
.replace(/[^a-zA-Z0-9]+/g, '-') // collapse non-alnum to dashes
|
|
150
|
-
.replace(/^-+|-+$/g, '')
|
|
151
|
-
.toLowerCase();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function stripTrailingIdSegment(slug) {
|
|
155
|
-
const normalized = toKebabCase(slug || '');
|
|
156
|
-
const segments = normalized.split('-');
|
|
157
|
-
if (segments.length <= 1) return normalized;
|
|
158
|
-
const last = segments[segments.length - 1];
|
|
159
|
-
if (/^[a-z0-9]{5,}$/i.test(last)) {
|
|
160
|
-
segments.pop();
|
|
161
|
-
}
|
|
162
|
-
return segments.join('-');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function sanitizeIdentifierForFilename(identifier) {
|
|
166
|
-
return String(identifier || '')
|
|
167
|
-
.normalize('NFKD')
|
|
168
|
-
.replace(/[\u0300-\u036f]/g, '') // remove accents
|
|
169
|
-
.replace(/[\u0000-\u001f\u007f-\u009f]/g, '') // remove control chars
|
|
170
|
-
.replace(/[<>:"/\\|?*]/g, '') // remove filesystem unsafe chars
|
|
171
|
-
.replace(/\.\./g, '') // remove parent directory traversal
|
|
172
|
-
.replace(/[/\\]+/g, '-') // replace all path separators with dashes
|
|
173
|
-
.replace(/^\.+/g, '') // remove leading dots
|
|
174
|
-
.replace(/\.+$/g, '') // remove trailing dots
|
|
175
|
-
.replace(/^-+|-+$/g, '') // trim edge dashes
|
|
176
|
-
.replace(/-+/g, '-') // collapse multiple dashes
|
|
177
|
-
.toLowerCase()
|
|
178
|
-
|| 'unknown'; // fallback for empty results
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function downloadStack(baseUrl, stackId) {
|
|
182
|
-
return new Promise((resolve, reject) => {
|
|
183
|
-
const useHttps = shouldUseHttps(baseUrl);
|
|
184
|
-
const protocol = useHttps ? https : http;
|
|
185
|
-
const scheme = useHttps ? 'https' : 'http';
|
|
186
|
-
const url = `${scheme}://${baseUrl}${STACK_API_PREFIX}${stackId}${STACK_API_SUFFIX}`;
|
|
187
|
-
|
|
188
|
-
protocol
|
|
189
|
-
.get(url, (res) => {
|
|
190
|
-
if (res.statusCode === 200) {
|
|
191
|
-
let data = '';
|
|
192
|
-
res.on('data', (chunk) => {
|
|
193
|
-
data += chunk;
|
|
194
|
-
});
|
|
195
|
-
res.on('end', () => {
|
|
196
|
-
try {
|
|
197
|
-
const parsed = JSON.parse(data);
|
|
198
|
-
resolve(parsed);
|
|
199
|
-
} catch (parseError) {
|
|
200
|
-
reject(new Error('Received invalid JSON while downloading stack'));
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
} else if (res.statusCode === 404) {
|
|
204
|
-
reject(new Error(`Stack '${stackId}' not found (404)`));
|
|
205
|
-
} else if (useHttps && (res.statusCode === 500 || res.statusCode >= 400)) {
|
|
206
|
-
if (canFallbackToHttp(baseUrl)) {
|
|
207
|
-
console.log(`HTTPS failed (${res.statusCode}), trying HTTP...`);
|
|
208
|
-
downloadStackWithHttp(baseUrl, stackId)
|
|
209
|
-
.then(resolve)
|
|
210
|
-
.catch(reject);
|
|
211
|
-
} else {
|
|
212
|
-
reject(new Error(`HTTPS failed (${res.statusCode}) and HTTP fallback is disabled`));
|
|
213
|
-
}
|
|
214
|
-
} else {
|
|
215
|
-
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
.on('error', (err) => {
|
|
219
|
-
if (useHttps) {
|
|
220
|
-
if (canFallbackToHttp(baseUrl)) {
|
|
221
|
-
console.log(`HTTPS failed (${err.message}), trying HTTP...`);
|
|
222
|
-
downloadStackWithHttp(baseUrl, stackId)
|
|
223
|
-
.then(resolve)
|
|
224
|
-
.catch(reject);
|
|
225
|
-
} else {
|
|
226
|
-
reject(err);
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
reject(err);
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function downloadStackWithHttp(baseUrl, stackId) {
|
|
236
|
-
return new Promise((resolve, reject) => {
|
|
237
|
-
const url = `http://${baseUrl}${STACK_API_PREFIX}${stackId}${STACK_API_SUFFIX}`;
|
|
238
|
-
|
|
239
|
-
console.log(`Trying HTTP: ${url}`);
|
|
240
|
-
|
|
241
|
-
http
|
|
242
|
-
.get(url, (res) => {
|
|
243
|
-
if (res.statusCode === 200) {
|
|
244
|
-
let data = '';
|
|
245
|
-
res.on('data', (chunk) => {
|
|
246
|
-
data += chunk;
|
|
247
|
-
});
|
|
248
|
-
res.on('end', () => {
|
|
249
|
-
try {
|
|
250
|
-
const parsed = JSON.parse(data);
|
|
251
|
-
resolve(parsed);
|
|
252
|
-
} catch (parseError) {
|
|
253
|
-
reject(new Error('Received invalid JSON while downloading stack'));
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
} else if (res.statusCode === 404) {
|
|
257
|
-
reject(new Error(`Stack '${stackId}' not found (404)`));
|
|
258
|
-
} else {
|
|
259
|
-
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
260
|
-
}
|
|
261
|
-
})
|
|
262
|
-
.on('error', reject);
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function downloadRawWithHeaders(baseUrl, fullPath) {
|
|
267
|
-
return new Promise((resolve, reject) => {
|
|
268
|
-
const useHttps = shouldUseHttps(baseUrl);
|
|
269
|
-
const protocol = useHttps ? https : http;
|
|
270
|
-
const scheme = useHttps ? 'https' : 'http';
|
|
271
|
-
const url = `${scheme}://${baseUrl}${RAW_PATH}${fullPath}`;
|
|
272
|
-
|
|
273
|
-
const handleOk = (res) => {
|
|
274
|
-
let data = '';
|
|
275
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
276
|
-
res.on('end', () => resolve({ content: data, headers: res.headers }));
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const handleHttpsFailover = (reason) => {
|
|
280
|
-
if (useHttps && canFallbackToHttp(baseUrl)) {
|
|
281
|
-
console.log(`HTTPS failed (${reason}), trying HTTP...`);
|
|
282
|
-
downloadRawWithHeadersHttp(baseUrl, fullPath).then(resolve).catch(reject);
|
|
283
|
-
} else {
|
|
284
|
-
reject(new Error(typeof reason === 'string' ? reason : reason.message));
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
protocol
|
|
289
|
-
.get(url, (res) => {
|
|
290
|
-
if (res.statusCode === 200) return handleOk(res);
|
|
291
|
-
if (res.statusCode === 404) return reject(new Error(`Command '${fullPath}' not found (404)`));
|
|
292
|
-
return handleHttpsFailover(`${res.statusCode}`);
|
|
293
|
-
})
|
|
294
|
-
.on('error', handleHttpsFailover);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function downloadRawWithHeadersHttp(baseUrl, fullPath) {
|
|
299
|
-
return new Promise((resolve, reject) => {
|
|
300
|
-
const url = `http://${baseUrl}${RAW_PATH}${fullPath}`;
|
|
301
|
-
console.log(`Trying HTTP: ${url}`);
|
|
302
|
-
http
|
|
303
|
-
.get(url, (res) => {
|
|
304
|
-
if (res.statusCode === 200) {
|
|
305
|
-
let data = '';
|
|
306
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
307
|
-
res.on('end', () => resolve({ content: data, headers: res.headers }));
|
|
308
|
-
} else if (res.statusCode === 404) {
|
|
309
|
-
reject(new Error(`Command '${fullPath}' not found (404)`));
|
|
310
|
-
} else {
|
|
311
|
-
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
312
|
-
}
|
|
313
|
-
})
|
|
314
|
-
.on('error', reject);
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function findClosestFolder(folderName) {
|
|
319
|
-
let currentDir = process.cwd();
|
|
320
|
-
|
|
321
|
-
while (true) {
|
|
322
|
-
const candidate = path.join(currentDir, folderName);
|
|
323
|
-
if (fs.existsSync(candidate)) {
|
|
324
|
-
return candidate;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const parentDir = path.dirname(currentDir);
|
|
328
|
-
if (parentDir === currentDir) {
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
currentDir = parentDir;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function resolveClaudeDirectory(subFolder) {
|
|
338
|
-
const projectClaudeDir = findClosestFolder('.claude');
|
|
339
|
-
if (projectClaudeDir) {
|
|
340
|
-
return path.join(projectClaudeDir, subFolder);
|
|
341
|
-
}
|
|
342
|
-
return path.join(os.homedir(), '.claude', subFolder);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function resolveCursorDirectory(subFolder) {
|
|
346
|
-
const projectCursorDir = findClosestFolder('.cursor');
|
|
347
|
-
if (!projectCursorDir) {
|
|
348
|
-
const message =
|
|
349
|
-
'Unable to find a .cursor directory. Run this command from inside a Cursor project.';
|
|
350
|
-
throw new Error(message);
|
|
351
|
-
}
|
|
352
|
-
return path.join(projectCursorDir, subFolder);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function resolveClaudeSkillsDirectory() {
|
|
356
|
-
const projectClaudeDir = findClosestFolder('.claude');
|
|
357
|
-
if (!projectClaudeDir) {
|
|
358
|
-
const message =
|
|
359
|
-
'Unable to find a .claude directory. Run this command from inside a project with a .claude folder.';
|
|
360
|
-
throw new Error(message);
|
|
361
|
-
}
|
|
362
|
-
return path.join(projectClaudeDir, 'skills');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function ensureDirectoryExists(dirPath) {
|
|
366
|
-
if (!fs.existsSync(dirPath)) {
|
|
367
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
368
|
-
console.log(`Created directory: ${dirPath}`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function checkFileExists(commandsDir, filename) {
|
|
373
|
-
const filePath = path.join(commandsDir, filename);
|
|
374
|
-
return fs.existsSync(filePath);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function saveCommand({
|
|
378
|
-
directory,
|
|
379
|
-
filename,
|
|
380
|
-
content,
|
|
381
|
-
fullPath,
|
|
382
|
-
target,
|
|
383
|
-
}) {
|
|
384
|
-
const targetConfig = TARGET_CONFIG[target];
|
|
385
|
-
const isSkill = targetConfig && targetConfig.isSkill;
|
|
386
|
-
|
|
387
|
-
let filePath;
|
|
388
|
-
let displayPath;
|
|
389
|
-
let commandName;
|
|
390
|
-
|
|
391
|
-
if (isSkill) {
|
|
392
|
-
const skillName = filename.replace(path.extname(filename), '');
|
|
393
|
-
const skillDir = path.join(directory, skillName);
|
|
394
|
-
ensureDirectoryExists(skillDir);
|
|
395
|
-
filePath = path.join(skillDir, 'SKILL.md');
|
|
396
|
-
const relativeDir = path.relative(process.cwd(), skillDir);
|
|
397
|
-
displayPath = relativeDir
|
|
398
|
-
? path.join(relativeDir, 'SKILL.md')
|
|
399
|
-
: path.join(skillName, 'SKILL.md');
|
|
400
|
-
commandName = skillName;
|
|
401
|
-
} else {
|
|
402
|
-
filePath = path.join(directory, filename);
|
|
403
|
-
const relativeDir = path.relative(process.cwd(), directory);
|
|
404
|
-
displayPath = relativeDir
|
|
405
|
-
? path.join(relativeDir, filename)
|
|
406
|
-
: filename;
|
|
407
|
-
commandName = filename.replace(path.extname(filename), '');
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
console.log(`📁 Saving ${isSkill ? 'skill' : 'command'} to ${displayPath}`);
|
|
411
|
-
|
|
412
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
413
|
-
|
|
414
|
-
logSuccessMessage(target, fullPath, commandName);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function logSuccessMessage(target, fullPath, commandName) {
|
|
418
|
-
switch (target) {
|
|
419
|
-
case INSTALLATION_TARGETS.CLAUDE_SLASH:
|
|
420
|
-
console.log(`✅ Command '${fullPath}' installed successfully`);
|
|
421
|
-
console.log(`\n🎉 You can now use: /${commandName}`);
|
|
422
|
-
break;
|
|
423
|
-
case INSTALLATION_TARGETS.CLAUDE_SUB_AGENT:
|
|
424
|
-
console.log(`✅ Agent '${fullPath}' installed successfully`);
|
|
425
|
-
console.log(
|
|
426
|
-
`\n🎉 You can now select '${commandName}' from Claude Desktop's agent menu.`
|
|
427
|
-
);
|
|
428
|
-
break;
|
|
429
|
-
case INSTALLATION_TARGETS.CLAUDE_SKILL:
|
|
430
|
-
console.log(`✅ Skill '${fullPath}' installed successfully`);
|
|
431
|
-
console.log(
|
|
432
|
-
`\n🎉 Claude Code will now automatically use the '${commandName}' skill when relevant.`
|
|
433
|
-
);
|
|
434
|
-
break;
|
|
435
|
-
case INSTALLATION_TARGETS.CURSOR_COMMAND:
|
|
436
|
-
console.log(`✅ Cursor command '${fullPath}' installed successfully`);
|
|
437
|
-
console.log(
|
|
438
|
-
`\n🎉 Open Cursor and run '${commandName}' from the command palette.`
|
|
439
|
-
);
|
|
440
|
-
break;
|
|
441
|
-
case INSTALLATION_TARGETS.CURSOR_RULE:
|
|
442
|
-
console.log(`✅ Cursor rule '${fullPath}' installed successfully`);
|
|
443
|
-
console.log(
|
|
444
|
-
`\n🎉 Cursor will apply '${commandName}' to this project.`
|
|
445
|
-
);
|
|
446
|
-
break;
|
|
447
|
-
default:
|
|
448
|
-
console.log(`✅ '${fullPath}' installed successfully`);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function parseCommandArgs(args) {
|
|
453
|
-
let mode = MODES.COMMAND;
|
|
454
|
-
let identifier = null;
|
|
455
|
-
let target = null;
|
|
456
|
-
let showHelpFlag = false;
|
|
457
|
-
|
|
458
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
459
|
-
const arg = args[i];
|
|
460
|
-
|
|
461
|
-
if (arg === '--help' || arg === '-h') {
|
|
462
|
-
showHelpFlag = true;
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (arg.startsWith('--target')) {
|
|
467
|
-
let rawValue = null;
|
|
468
|
-
if (arg.includes('=')) {
|
|
469
|
-
rawValue = arg.split('=')[1];
|
|
470
|
-
} else {
|
|
471
|
-
rawValue = args[i + 1];
|
|
472
|
-
i += 1;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (!rawValue) {
|
|
476
|
-
throw new Error('Missing value for --target option');
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const normalizedTarget = rawValue.toLowerCase();
|
|
480
|
-
if (!KNOWN_TARGETS.includes(normalizedTarget)) {
|
|
481
|
-
throw new Error(
|
|
482
|
-
`Unknown target '${rawValue}'. Available targets: ${KNOWN_TARGETS.join(', ')}`
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
target = normalizedTarget;
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (arg === '-t') {
|
|
491
|
-
const rawValue = args[i + 1];
|
|
492
|
-
i += 1;
|
|
493
|
-
|
|
494
|
-
if (!rawValue) {
|
|
495
|
-
throw new Error('Missing value for -t option');
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const normalizedTarget = rawValue.toLowerCase();
|
|
499
|
-
if (!KNOWN_TARGETS.includes(normalizedTarget)) {
|
|
500
|
-
throw new Error(
|
|
501
|
-
`Unknown target '${rawValue}'. Available targets: ${KNOWN_TARGETS.join(', ')}`
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
target = normalizedTarget;
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (mode === MODES.COMMAND && arg === 'stack' && !identifier) {
|
|
510
|
-
mode = MODES.STACK;
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (!identifier) {
|
|
515
|
-
identifier = arg;
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
throw new Error(`Unknown argument: ${arg}`);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return { mode, identifier, target, showHelp: showHelpFlag };
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
async function main() {
|
|
526
|
-
const args = process.argv.slice(2);
|
|
527
|
-
|
|
528
|
-
try {
|
|
529
|
-
const {
|
|
530
|
-
mode,
|
|
531
|
-
identifier,
|
|
532
|
-
target,
|
|
533
|
-
showHelp: helpRequested,
|
|
534
|
-
} = parseCommandArgs(args);
|
|
535
|
-
|
|
536
|
-
if (helpRequested || args.length === 0) {
|
|
537
|
-
showHelp();
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
const baseUrl = normalizeBaseHost(process.env.IMPULSE_BASE_URL || DEFAULT_BASE_URL);
|
|
542
|
-
|
|
543
|
-
if (mode === MODES.COMMAND) {
|
|
544
|
-
if (!identifier) {
|
|
545
|
-
console.error('❌ Error: Missing command path');
|
|
546
|
-
showHelp();
|
|
547
|
-
process.exit(1);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (!identifier.includes('/')) {
|
|
551
|
-
console.error('❌ Error: Path must include user/command format');
|
|
552
|
-
showHelp();
|
|
553
|
-
process.exit(1);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const resolvedTarget = target || DEFAULT_TARGET;
|
|
557
|
-
const targetConfig = TARGET_CONFIG[resolvedTarget];
|
|
558
|
-
|
|
559
|
-
if (!targetConfig) {
|
|
560
|
-
console.error(`❌ Error: Unsupported target '${resolvedTarget}'`);
|
|
561
|
-
process.exit(1);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const directory = targetConfig.resolveDirectory();
|
|
565
|
-
|
|
566
|
-
ensureDirectoryExists(directory);
|
|
567
|
-
|
|
568
|
-
const commandNameFromPath = identifier.split('/').pop();
|
|
569
|
-
// First download the content to capture headers (and title)
|
|
570
|
-
const { content, headers } = await downloadRawWithHeaders(baseUrl, identifier);
|
|
571
|
-
const titleHeader = headers['x-prompt-title'];
|
|
572
|
-
const title = Array.isArray(titleHeader) ? titleHeader[0] : titleHeader;
|
|
573
|
-
|
|
574
|
-
const baseNameCandidate = title
|
|
575
|
-
? toKebabCase(title)
|
|
576
|
-
: stripTrailingIdSegment(commandNameFromPath);
|
|
577
|
-
const finalBaseName = sanitizeIdentifierForFilename(baseNameCandidate);
|
|
578
|
-
|
|
579
|
-
// Overwrite if exists; do not append author or random suffix
|
|
580
|
-
const filename = `${finalBaseName}${targetConfig.extension}`;
|
|
581
|
-
|
|
582
|
-
saveCommand({
|
|
583
|
-
directory,
|
|
584
|
-
filename,
|
|
585
|
-
content,
|
|
586
|
-
fullPath: identifier,
|
|
587
|
-
target: resolvedTarget,
|
|
588
|
-
});
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
if (!identifier) {
|
|
593
|
-
console.error('❌ Error: Missing stack identifier');
|
|
594
|
-
showHelp();
|
|
595
|
-
process.exit(1);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const stackPayload = await downloadStack(baseUrl, identifier);
|
|
599
|
-
const stack = stackPayload.stack || {};
|
|
600
|
-
const prompts = Array.isArray(stackPayload.prompts)
|
|
601
|
-
? stackPayload.prompts
|
|
602
|
-
: [];
|
|
603
|
-
|
|
604
|
-
console.log(
|
|
605
|
-
`📦 Installing stack '${stack.title || identifier}' (${prompts.length} prompt${
|
|
606
|
-
prompts.length === 1 ? '' : 's'
|
|
607
|
-
})`
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
let installed = 0;
|
|
611
|
-
let skipped = 0;
|
|
612
|
-
|
|
613
|
-
for (let index = 0; index < prompts.length; index += 1) {
|
|
614
|
-
const prompt = prompts[index];
|
|
615
|
-
const commandPath = prompt.path || prompt.slug || prompt.id || `prompt-${index + 1}`;
|
|
616
|
-
const baseName = prompt.title
|
|
617
|
-
? toKebabCase(prompt.title)
|
|
618
|
-
: stripTrailingIdSegment(prompt.slug || `prompt-${index + 1}`);
|
|
619
|
-
|
|
620
|
-
const resolvedTarget = target || prompt.target || DEFAULT_TARGET;
|
|
621
|
-
const targetConfig = TARGET_CONFIG[resolvedTarget];
|
|
622
|
-
if (!targetConfig) {
|
|
623
|
-
console.warn(`⚠️ Skipping prompt '${commandPath}' (unsupported target '${resolvedTarget}')`);
|
|
624
|
-
skipped += 1;
|
|
625
|
-
continue;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const directory = targetConfig.resolveDirectory();
|
|
629
|
-
ensureDirectoryExists(directory);
|
|
630
|
-
|
|
631
|
-
// Sanitize and overwrite if exists (no author or random suffix)
|
|
632
|
-
const sanitizedBaseName = sanitizeIdentifierForFilename(baseName);
|
|
633
|
-
const filename = `${sanitizedBaseName}${targetConfig.extension}`;
|
|
634
|
-
|
|
635
|
-
if (!prompt.markdown) {
|
|
636
|
-
console.warn(`⚠️ Skipping prompt '${commandPath}' (no content available)`);
|
|
637
|
-
skipped += 1;
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
saveCommand({
|
|
642
|
-
directory,
|
|
643
|
-
filename,
|
|
644
|
-
content: prompt.markdown,
|
|
645
|
-
fullPath: commandPath,
|
|
646
|
-
target: resolvedTarget,
|
|
647
|
-
});
|
|
648
|
-
installed += 1;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
console.log(
|
|
652
|
-
`🎉 Installed ${installed} prompt${installed === 1 ? '' : 's'} from stack '${
|
|
653
|
-
stack.title || identifier
|
|
654
|
-
}'${skipped ? ` (skipped ${skipped})` : ''}`
|
|
655
|
-
);
|
|
656
|
-
} catch (error) {
|
|
657
|
-
console.error(`❌ Error: ${error.message}`);
|
|
658
|
-
process.exit(1);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (require.main === module) {
|
|
663
|
-
main();
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
module.exports = { main };
|