@impulselab/directory 1.0.5 → 1.0.7
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/index.js +124 -31
- package/package.json +7 -11
package/index.js
CHANGED
|
@@ -133,6 +133,60 @@ function shouldUseHttps(baseUrl) {
|
|
|
133
133
|
return true;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
function toKebabCase(input) {
|
|
137
|
+
return String(input)
|
|
138
|
+
.normalize('NFKD') // strip accents
|
|
139
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
140
|
+
.replace(/['"]/g, '')
|
|
141
|
+
.replace(/[^a-zA-Z0-9]+/g, '-') // collapse non-alnum to dashes
|
|
142
|
+
.replace(/^-+|-+$/g, '')
|
|
143
|
+
.toLowerCase();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stripTrailingIdSegment(slug) {
|
|
147
|
+
const normalized = toKebabCase(slug || '');
|
|
148
|
+
const segments = normalized.split('-');
|
|
149
|
+
if (segments.length <= 1) return normalized;
|
|
150
|
+
const last = segments[segments.length - 1];
|
|
151
|
+
if (/^[a-z0-9]{5,}$/i.test(last)) {
|
|
152
|
+
segments.pop();
|
|
153
|
+
}
|
|
154
|
+
return segments.join('-');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sanitizeIdentifierForFilename(identifier) {
|
|
158
|
+
return String(identifier || '')
|
|
159
|
+
.normalize('NFKD')
|
|
160
|
+
.replace(/[\u0300-\u036f]/g, '') // remove accents
|
|
161
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, '') // remove control chars
|
|
162
|
+
.replace(/[<>:"/\\|?*]/g, '') // remove filesystem unsafe chars
|
|
163
|
+
.replace(/\.\./g, '') // remove parent directory traversal
|
|
164
|
+
.replace(/[/\\]+/g, '-') // replace all path separators with dashes
|
|
165
|
+
.replace(/^\.+/g, '') // remove leading dots
|
|
166
|
+
.replace(/\.+$/g, '') // remove trailing dots
|
|
167
|
+
.replace(/^-+|-+$/g, '') // trim edge dashes
|
|
168
|
+
.replace(/-+/g, '-') // collapse multiple dashes
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
|| 'unknown'; // fallback for empty results
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function generateUniqueFilename(directory, baseName, extension) {
|
|
174
|
+
const baseFilename = `${baseName}${extension}`;
|
|
175
|
+
|
|
176
|
+
if (!checkFileExists(directory, baseFilename)) {
|
|
177
|
+
return baseFilename;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let counter = 1;
|
|
181
|
+
let candidate = `${baseName}-${counter}${extension}`;
|
|
182
|
+
while (checkFileExists(directory, candidate)) {
|
|
183
|
+
counter += 1;
|
|
184
|
+
candidate = `${baseName}-${counter}${extension}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return candidate;
|
|
188
|
+
}
|
|
189
|
+
|
|
136
190
|
function downloadCommand(baseUrl, fullPath) {
|
|
137
191
|
return new Promise((resolve, reject) => {
|
|
138
192
|
const useHttps = shouldUseHttps(baseUrl);
|
|
@@ -290,6 +344,58 @@ function downloadStackWithHttp(baseUrl, stackId) {
|
|
|
290
344
|
});
|
|
291
345
|
}
|
|
292
346
|
|
|
347
|
+
function downloadRawWithHeaders(baseUrl, fullPath) {
|
|
348
|
+
return new Promise((resolve, reject) => {
|
|
349
|
+
const useHttps = shouldUseHttps(baseUrl);
|
|
350
|
+
const protocol = useHttps ? https : http;
|
|
351
|
+
const scheme = useHttps ? 'https' : 'http';
|
|
352
|
+
const url = `${scheme}://${baseUrl}${RAW_PATH}${fullPath}`;
|
|
353
|
+
|
|
354
|
+
const handleOk = (res) => {
|
|
355
|
+
let data = '';
|
|
356
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
357
|
+
res.on('end', () => resolve({ content: data, headers: res.headers }));
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const handleHttpsFailover = (reason) => {
|
|
361
|
+
if (useHttps && canFallbackToHttp(baseUrl)) {
|
|
362
|
+
console.log(`HTTPS failed (${reason}), trying HTTP...`);
|
|
363
|
+
downloadRawWithHeadersHttp(baseUrl, fullPath).then(resolve).catch(reject);
|
|
364
|
+
} else {
|
|
365
|
+
reject(new Error(typeof reason === 'string' ? reason : reason.message));
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
protocol
|
|
370
|
+
.get(url, (res) => {
|
|
371
|
+
if (res.statusCode === 200) return handleOk(res);
|
|
372
|
+
if (res.statusCode === 404) return reject(new Error(`Command '${fullPath}' not found (404)`));
|
|
373
|
+
return handleHttpsFailover(`${res.statusCode}`);
|
|
374
|
+
})
|
|
375
|
+
.on('error', handleHttpsFailover);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function downloadRawWithHeadersHttp(baseUrl, fullPath) {
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
const url = `http://${baseUrl}${RAW_PATH}${fullPath}`;
|
|
382
|
+
console.log(`Trying HTTP: ${url}`);
|
|
383
|
+
http
|
|
384
|
+
.get(url, (res) => {
|
|
385
|
+
if (res.statusCode === 200) {
|
|
386
|
+
let data = '';
|
|
387
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
388
|
+
res.on('end', () => resolve({ content: data, headers: res.headers }));
|
|
389
|
+
} else if (res.statusCode === 404) {
|
|
390
|
+
reject(new Error(`Command '${fullPath}' not found (404)`));
|
|
391
|
+
} else {
|
|
392
|
+
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
.on('error', reject);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
293
399
|
function findClosestFolder(folderName) {
|
|
294
400
|
let currentDir = process.cwd();
|
|
295
401
|
|
|
@@ -505,13 +611,19 @@ async function main() {
|
|
|
505
611
|
|
|
506
612
|
ensureDirectoryExists(directory);
|
|
507
613
|
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
const
|
|
614
|
+
const commandNameFromPath = identifier.split('/').pop();
|
|
615
|
+
// First download the content to capture headers (and title)
|
|
616
|
+
const { content, headers } = await downloadRawWithHeaders(baseUrl, identifier);
|
|
617
|
+
const titleHeader = headers['x-prompt-title'];
|
|
618
|
+
const title = Array.isArray(titleHeader) ? titleHeader[0] : titleHeader;
|
|
513
619
|
|
|
514
|
-
const
|
|
620
|
+
const baseNameCandidate = title
|
|
621
|
+
? toKebabCase(title)
|
|
622
|
+
: stripTrailingIdSegment(commandNameFromPath);
|
|
623
|
+
const finalBaseName = sanitizeIdentifierForFilename(baseNameCandidate);
|
|
624
|
+
|
|
625
|
+
// Overwrite if exists; do not append author or random suffix
|
|
626
|
+
const filename = `${finalBaseName}${targetConfig.extension}`;
|
|
515
627
|
|
|
516
628
|
saveCommand({
|
|
517
629
|
directory,
|
|
@@ -547,7 +659,9 @@ async function main() {
|
|
|
547
659
|
for (let index = 0; index < prompts.length; index += 1) {
|
|
548
660
|
const prompt = prompts[index];
|
|
549
661
|
const commandPath = prompt.path || prompt.slug || prompt.id || `prompt-${index + 1}`;
|
|
550
|
-
const baseName = prompt.
|
|
662
|
+
const baseName = prompt.title
|
|
663
|
+
? toKebabCase(prompt.title)
|
|
664
|
+
: stripTrailingIdSegment(prompt.slug || `prompt-${index + 1}`);
|
|
551
665
|
|
|
552
666
|
const resolvedTarget = target || prompt.target || DEFAULT_TARGET;
|
|
553
667
|
const targetConfig = TARGET_CONFIG[resolvedTarget];
|
|
@@ -560,30 +674,9 @@ async function main() {
|
|
|
560
674
|
const directory = targetConfig.resolveDirectory();
|
|
561
675
|
ensureDirectoryExists(directory);
|
|
562
676
|
|
|
563
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
: `${stack.id || identifier}-${baseName}`;
|
|
567
|
-
const fallbackFilename = `${fallbackBase}${targetConfig.extension}`;
|
|
568
|
-
|
|
569
|
-
const simpleExists = checkFileExists(directory, simpleFilename);
|
|
570
|
-
const fallbackExists = checkFileExists(directory, fallbackFilename);
|
|
571
|
-
|
|
572
|
-
let filename = simpleFilename;
|
|
573
|
-
if (simpleExists) {
|
|
574
|
-
filename = fallbackFilename;
|
|
575
|
-
}
|
|
576
|
-
if ((simpleExists && fallbackExists) || checkFileExists(directory, filename)) {
|
|
577
|
-
const baseForUniq = fallbackBase;
|
|
578
|
-
const ext = targetConfig.extension;
|
|
579
|
-
let counter = 1;
|
|
580
|
-
let candidate = `${baseForUniq}-${counter}${ext}`;
|
|
581
|
-
while (checkFileExists(directory, candidate)) {
|
|
582
|
-
counter += 1;
|
|
583
|
-
candidate = `${baseForUniq}-${counter}${ext}`;
|
|
584
|
-
}
|
|
585
|
-
filename = candidate;
|
|
586
|
-
}
|
|
677
|
+
// Sanitize and overwrite if exists (no author or random suffix)
|
|
678
|
+
const sanitizedBaseName = sanitizeIdentifierForFilename(baseName);
|
|
679
|
+
const filename = `${sanitizedBaseName}${targetConfig.extension}`;
|
|
587
680
|
|
|
588
681
|
if (!prompt.markdown) {
|
|
589
682
|
console.warn(`⚠️ Skipping prompt '${commandPath}' (no content available)`);
|
package/package.json
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@impulselab/directory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Download and install Claude commands from Impulse Directory",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"directory": "./index.js"
|
|
8
8
|
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "node index.js --help",
|
|
11
|
-
"predeploy": "chmod +x index.js",
|
|
12
|
-
"deploy": "pnpm publish --access public --no-git-checks"
|
|
13
|
-
},
|
|
14
9
|
"keywords": [
|
|
15
10
|
"claude",
|
|
16
11
|
"commands",
|
|
@@ -19,15 +14,16 @@
|
|
|
19
14
|
],
|
|
20
15
|
"author": "Impulse Lab <team@impulselab.ai> (https://impulselab.ai)",
|
|
21
16
|
"homepage": "https://impulselab.ai",
|
|
22
|
-
"repository": {
|
|
23
|
-
"type": "git",
|
|
24
|
-
"url": "git+https://github.com/impulselab-ai/impulse-directory.git"
|
|
25
|
-
},
|
|
26
17
|
"license": "MIT",
|
|
27
18
|
"engines": {
|
|
28
19
|
"node": ">=14"
|
|
29
20
|
},
|
|
30
21
|
"publishConfig": {
|
|
31
22
|
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node index.js --help",
|
|
26
|
+
"predeploy": "chmod +x index.js",
|
|
27
|
+
"deploy": "pnpm publish --access public --no-git-checks"
|
|
32
28
|
}
|
|
33
|
-
}
|
|
29
|
+
}
|