@impulselab/directory 1.0.7 → 1.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/index.js +902 -566
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const http = require('http');
|
|
7
7
|
const os = require('os');
|
|
8
|
+
const readline = require('readline');
|
|
8
9
|
|
|
9
10
|
const PACKAGE = {
|
|
10
11
|
scopeName: '@impulselab/directory',
|
|
@@ -18,6 +19,13 @@ const DEFAULT_BASE_URL = 'impulse.directory';
|
|
|
18
19
|
const RAW_PATH = '/raw/';
|
|
19
20
|
const STACK_API_PREFIX = '/api/stacks/';
|
|
20
21
|
const STACK_API_SUFFIX = '/npx';
|
|
22
|
+
const DEVICE_CODE_PATH = '/api/auth/device/code';
|
|
23
|
+
const DEVICE_TOKEN_PATH = '/api/auth/device/token';
|
|
24
|
+
const SESSION_PATH = '/api/auth/get-session';
|
|
25
|
+
const CLI_CLIENT_ID = 'impulse-directory-cli';
|
|
26
|
+
|
|
27
|
+
const CONFIG_DIR = path.join(os.homedir(), '.impulse-directory');
|
|
28
|
+
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
21
29
|
|
|
22
30
|
function normalizeBaseHost(input) {
|
|
23
31
|
const raw = (input || '').trim();
|
|
@@ -40,51 +48,361 @@ function canFallbackToHttp(host) {
|
|
|
40
48
|
return process.env.ALLOW_HTTP_FALLBACK === '1' || isLocalHost(host);
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
function shouldUseHttps(baseUrl) {
|
|
52
|
+
if (baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1')) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
43
58
|
const MODES = {
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
COMMAND: 'command',
|
|
60
|
+
STACK: 'stack',
|
|
61
|
+
LOGIN: 'login',
|
|
62
|
+
LOGOUT: 'logout',
|
|
63
|
+
WHOAMI: 'whoami',
|
|
46
64
|
};
|
|
47
65
|
|
|
48
66
|
const INSTALLATION_TARGETS = {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
CLAUDE_SLASH: 'claude-slash',
|
|
68
|
+
CLAUDE_SUB_AGENT: 'claude-sub-agent',
|
|
69
|
+
CURSOR_COMMAND: 'cursor-command',
|
|
70
|
+
CURSOR_RULE: 'cursor-rule',
|
|
53
71
|
};
|
|
54
72
|
|
|
55
73
|
const DEFAULT_TARGET = INSTALLATION_TARGETS.CLAUDE_SLASH;
|
|
56
74
|
|
|
57
75
|
const TARGET_CONFIG = {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
[INSTALLATION_TARGETS.CLAUDE_SLASH]: {
|
|
77
|
+
label: 'Claude slash command',
|
|
78
|
+
description: '.claude/commands (falls back to ~/.claude/commands)',
|
|
79
|
+
resolveDirectory: () => resolveClaudeDirectory('commands'),
|
|
80
|
+
extension: '.md',
|
|
81
|
+
},
|
|
82
|
+
[INSTALLATION_TARGETS.CLAUDE_SUB_AGENT]: {
|
|
83
|
+
label: 'Claude sub agent',
|
|
84
|
+
description: '.claude/agents (falls back to ~/.claude/agents)',
|
|
85
|
+
resolveDirectory: () => resolveClaudeDirectory('agents'),
|
|
86
|
+
extension: '.md',
|
|
87
|
+
},
|
|
88
|
+
[INSTALLATION_TARGETS.CURSOR_COMMAND]: {
|
|
89
|
+
label: 'Cursor command',
|
|
90
|
+
description: '.cursor/commands (project only)',
|
|
91
|
+
resolveDirectory: () => resolveCursorDirectory('commands'),
|
|
92
|
+
extension: '.md',
|
|
93
|
+
},
|
|
94
|
+
[INSTALLATION_TARGETS.CURSOR_RULE]: {
|
|
95
|
+
label: 'Cursor rule',
|
|
96
|
+
description: '.cursor/rules (project only)',
|
|
97
|
+
resolveDirectory: () => resolveCursorDirectory('rules'),
|
|
98
|
+
extension: '.mdc',
|
|
99
|
+
},
|
|
82
100
|
};
|
|
83
101
|
|
|
84
102
|
const KNOWN_TARGETS = Object.keys(TARGET_CONFIG);
|
|
85
103
|
|
|
104
|
+
// ============================================
|
|
105
|
+
// Credentials Management
|
|
106
|
+
// ============================================
|
|
107
|
+
|
|
108
|
+
function ensureConfigDir() {
|
|
109
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
110
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function loadCredentials() {
|
|
115
|
+
try {
|
|
116
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
117
|
+
const data = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
|
|
118
|
+
return JSON.parse(data);
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Ignore errors, return null
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function saveCredentials(credentials) {
|
|
127
|
+
ensureConfigDir();
|
|
128
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), {
|
|
129
|
+
encoding: 'utf8',
|
|
130
|
+
mode: 0o600,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function clearCredentials() {
|
|
135
|
+
try {
|
|
136
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
137
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Ignore errors
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getAuthToken() {
|
|
145
|
+
const creds = loadCredentials();
|
|
146
|
+
if (!creds) return null;
|
|
147
|
+
|
|
148
|
+
// Check if token is expired
|
|
149
|
+
if (creds.expiresAt && new Date(creds.expiresAt) <= new Date()) {
|
|
150
|
+
console.log('⚠️ Session expired. Please run: npx @impulselab/directory login');
|
|
151
|
+
clearCredentials();
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return creds.accessToken || null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================
|
|
159
|
+
// HTTP Request Helpers
|
|
160
|
+
// ============================================
|
|
161
|
+
|
|
162
|
+
function httpRequest(options, body = null) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const protocol = options.protocol === 'http:' ? http : https;
|
|
165
|
+
const req = protocol.request(options, (res) => {
|
|
166
|
+
let data = '';
|
|
167
|
+
res.on('data', (chunk) => {
|
|
168
|
+
data += chunk;
|
|
169
|
+
});
|
|
170
|
+
res.on('end', () => {
|
|
171
|
+
resolve({ statusCode: res.statusCode, headers: res.headers, body: data });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
req.on('error', reject);
|
|
175
|
+
if (body) {
|
|
176
|
+
req.write(body);
|
|
177
|
+
}
|
|
178
|
+
req.end();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sleep(ms) {
|
|
183
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================
|
|
187
|
+
// Device Authorization Flow
|
|
188
|
+
// ============================================
|
|
189
|
+
|
|
190
|
+
async function requestDeviceCode(baseUrl) {
|
|
191
|
+
const useHttps = shouldUseHttps(baseUrl);
|
|
192
|
+
const protocol = useHttps ? 'https:' : 'http:';
|
|
193
|
+
const [hostname, port] = baseUrl.split(':');
|
|
194
|
+
|
|
195
|
+
const body = JSON.stringify({
|
|
196
|
+
client_id: CLI_CLIENT_ID,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const options = {
|
|
200
|
+
protocol,
|
|
201
|
+
hostname,
|
|
202
|
+
port: port || (useHttps ? 443 : 80),
|
|
203
|
+
path: DEVICE_CODE_PATH,
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: {
|
|
206
|
+
'Content-Type': 'application/json',
|
|
207
|
+
'Content-Length': Buffer.byteLength(body),
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const res = await httpRequest(options, body);
|
|
212
|
+
|
|
213
|
+
if (res.statusCode !== 200) {
|
|
214
|
+
throw new Error(`Failed to request device code: HTTP ${res.statusCode}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return JSON.parse(res.body);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function pollForToken(baseUrl, deviceCode, interval) {
|
|
221
|
+
const useHttps = shouldUseHttps(baseUrl);
|
|
222
|
+
const protocol = useHttps ? 'https:' : 'http:';
|
|
223
|
+
const [hostname, port] = baseUrl.split(':');
|
|
224
|
+
|
|
225
|
+
const body = JSON.stringify({
|
|
226
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
227
|
+
device_code: deviceCode,
|
|
228
|
+
client_id: CLI_CLIENT_ID,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const options = {
|
|
232
|
+
protocol,
|
|
233
|
+
hostname,
|
|
234
|
+
port: port || (useHttps ? 443 : 80),
|
|
235
|
+
path: DEVICE_TOKEN_PATH,
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
'Content-Type': 'application/json',
|
|
239
|
+
'Content-Length': Buffer.byteLength(body),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
while (true) {
|
|
244
|
+
await sleep(interval * 1000);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const res = await httpRequest(options, body);
|
|
248
|
+
const data = JSON.parse(res.body);
|
|
249
|
+
|
|
250
|
+
if (res.statusCode === 200) {
|
|
251
|
+
return data;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (data.error === 'authorization_pending') {
|
|
255
|
+
process.stdout.write('.');
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (data.error === 'slow_down') {
|
|
260
|
+
interval = Math.min(interval + 1, 10);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (data.error === 'access_denied') {
|
|
265
|
+
throw new Error('Authorization was denied by the user');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (data.error === 'expired_token') {
|
|
269
|
+
throw new Error('Device code expired. Please try again.');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
throw new Error(data.error_description || data.error || 'Unknown error');
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (err.message.includes('authorization_pending')) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function fetchSession(baseUrl, token) {
|
|
283
|
+
const useHttps = shouldUseHttps(baseUrl);
|
|
284
|
+
const protocol = useHttps ? 'https:' : 'http:';
|
|
285
|
+
const [hostname, port] = baseUrl.split(':');
|
|
286
|
+
|
|
287
|
+
const options = {
|
|
288
|
+
protocol,
|
|
289
|
+
hostname,
|
|
290
|
+
port: port || (useHttps ? 443 : 80),
|
|
291
|
+
path: SESSION_PATH,
|
|
292
|
+
method: 'GET',
|
|
293
|
+
headers: {
|
|
294
|
+
Authorization: `Bearer ${token}`,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const res = await httpRequest(options);
|
|
299
|
+
|
|
300
|
+
if (res.statusCode !== 200) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
return JSON.parse(res.body);
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function doLogin(baseUrl) {
|
|
312
|
+
console.log('🔐 Authenticating with Impulse Directory...\n');
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const deviceCodeResponse = await requestDeviceCode(baseUrl);
|
|
316
|
+
const {
|
|
317
|
+
user_code: userCode,
|
|
318
|
+
verification_uri: verificationUri,
|
|
319
|
+
verification_uri_complete: verificationUriComplete,
|
|
320
|
+
device_code: deviceCode,
|
|
321
|
+
expires_in: expiresIn,
|
|
322
|
+
interval = 5,
|
|
323
|
+
} = deviceCodeResponse;
|
|
324
|
+
|
|
325
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
326
|
+
console.log('');
|
|
327
|
+
console.log(' To authenticate, visit:');
|
|
328
|
+
console.log(` ${verificationUriComplete || verificationUri}`);
|
|
329
|
+
console.log('');
|
|
330
|
+
if (!verificationUriComplete) {
|
|
331
|
+
console.log(` And enter code: ${userCode}`);
|
|
332
|
+
console.log('');
|
|
333
|
+
}
|
|
334
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
335
|
+
console.log('');
|
|
336
|
+
console.log(`⏳ Waiting for authorization (expires in ${Math.floor(expiresIn / 60)} minutes)`);
|
|
337
|
+
|
|
338
|
+
const tokenResponse = await pollForToken(baseUrl, deviceCode, interval);
|
|
339
|
+
console.log('\n');
|
|
340
|
+
|
|
341
|
+
// Save credentials
|
|
342
|
+
saveCredentials({
|
|
343
|
+
accessToken: tokenResponse.access_token,
|
|
344
|
+
tokenType: tokenResponse.token_type,
|
|
345
|
+
expiresIn: tokenResponse.expires_in,
|
|
346
|
+
expiresAt: tokenResponse.expires_in
|
|
347
|
+
? new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString()
|
|
348
|
+
: null,
|
|
349
|
+
scope: tokenResponse.scope,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Try to fetch user info
|
|
353
|
+
const session = await fetchSession(baseUrl, tokenResponse.access_token);
|
|
354
|
+
if (session && session.user) {
|
|
355
|
+
console.log(`✅ Logged in as ${session.user.name || session.user.email}`);
|
|
356
|
+
} else {
|
|
357
|
+
console.log('✅ Successfully authenticated!');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
console.log('\nYou can now download private prompts and stacks.');
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.error(`\n❌ Login failed: ${error.message}`);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function doLogout() {
|
|
368
|
+
const creds = loadCredentials();
|
|
369
|
+
if (!creds) {
|
|
370
|
+
console.log('ℹ️ Not currently logged in.');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
clearCredentials();
|
|
375
|
+
console.log('✅ Logged out successfully.');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function doWhoami(baseUrl) {
|
|
379
|
+
const token = getAuthToken();
|
|
380
|
+
if (!token) {
|
|
381
|
+
console.log('ℹ️ Not logged in.');
|
|
382
|
+
console.log(' Run: npx @impulselab/directory login');
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const session = await fetchSession(baseUrl, token);
|
|
388
|
+
if (session && session.user) {
|
|
389
|
+
console.log(`👤 Logged in as: ${session.user.name || 'Unknown'}`);
|
|
390
|
+
console.log(` Email: ${session.user.email}`);
|
|
391
|
+
} else {
|
|
392
|
+
console.log('⚠️ Session is invalid or expired.');
|
|
393
|
+
console.log(' Run: npx @impulselab/directory login');
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
console.log('⚠️ Could not verify session.');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ============================================
|
|
401
|
+
// Help
|
|
402
|
+
// ============================================
|
|
403
|
+
|
|
86
404
|
function showHelp() {
|
|
87
|
-
const primaryUsageCmd = `npx
|
|
405
|
+
const primaryUsageCmd = `npx @impulselab/directory`;
|
|
88
406
|
console.log(`
|
|
89
407
|
Impulse Directory Command Installer
|
|
90
408
|
Made by ${PACKAGE.brand} (${PACKAGE.websiteUrl})
|
|
@@ -92,16 +410,24 @@ Made by ${PACKAGE.brand} (${PACKAGE.websiteUrl})
|
|
|
92
410
|
Usage:
|
|
93
411
|
${primaryUsageCmd} <user-slug>/<command-slug> [--target <target>]
|
|
94
412
|
${primaryUsageCmd} stack <stack-id>
|
|
95
|
-
|
|
413
|
+
${primaryUsageCmd} login
|
|
414
|
+
${primaryUsageCmd} logout
|
|
415
|
+
${primaryUsageCmd} whoami
|
|
96
416
|
|
|
97
417
|
Description:
|
|
98
418
|
Downloads a command from Impulse Directory and installs it in the right tool folder.
|
|
99
419
|
When using the stack mode, installs each prompt in the stack to its inferred target.
|
|
420
|
+
Use 'login' to authenticate and access private prompts.
|
|
100
421
|
|
|
101
422
|
Arguments:
|
|
102
423
|
user-slug/command-slug The full path to the command (user/command)
|
|
103
424
|
stack-id The stack ULID shown in the Impulse Directory UI
|
|
104
425
|
|
|
426
|
+
Commands:
|
|
427
|
+
login Authenticate with your Impulse Directory account
|
|
428
|
+
logout Clear stored credentials
|
|
429
|
+
whoami Show current authentication status
|
|
430
|
+
|
|
105
431
|
Options:
|
|
106
432
|
--target <target> Installation target (optional for single commands)
|
|
107
433
|
-t <target> Short alias for --target
|
|
@@ -119,594 +445,604 @@ Environment Variables:
|
|
|
119
445
|
Examples: impulse.directory | localhost:3000
|
|
120
446
|
|
|
121
447
|
Examples:
|
|
448
|
+
${primaryUsageCmd} login
|
|
122
449
|
${primaryUsageCmd} john/my-awesome-command
|
|
450
|
+
${primaryUsageCmd} john/my-private-command (requires login)
|
|
123
451
|
${primaryUsageCmd} john/my-awesome-command --target claude-sub-agent
|
|
124
|
-
${primaryUsageCmd}
|
|
125
|
-
${primaryUsageCmd} stack 01JH0000000ABCDEF --target claude-slash
|
|
452
|
+
${primaryUsageCmd} stack 01JH0000000ABCDEF
|
|
126
453
|
`);
|
|
127
454
|
}
|
|
128
455
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
456
|
+
// ============================================
|
|
457
|
+
// Download Functions (with auth support)
|
|
458
|
+
// ============================================
|
|
135
459
|
|
|
136
460
|
function toKebabCase(input) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
461
|
+
return String(input)
|
|
462
|
+
.normalize('NFKD')
|
|
463
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
464
|
+
.replace(/['"]/g, '')
|
|
465
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
466
|
+
.replace(/^-+|-+$/g, '')
|
|
467
|
+
.toLowerCase();
|
|
144
468
|
}
|
|
145
469
|
|
|
146
470
|
function stripTrailingIdSegment(slug) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
471
|
+
const normalized = toKebabCase(slug || '');
|
|
472
|
+
const segments = normalized.split('-');
|
|
473
|
+
if (segments.length <= 1) return normalized;
|
|
474
|
+
const last = segments[segments.length - 1];
|
|
475
|
+
if (/^[a-z0-9]{5,}$/i.test(last)) {
|
|
476
|
+
segments.pop();
|
|
477
|
+
}
|
|
478
|
+
return segments.join('-');
|
|
155
479
|
}
|
|
156
480
|
|
|
157
481
|
function sanitizeIdentifierForFilename(identifier) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
190
|
-
function downloadCommand(baseUrl, fullPath) {
|
|
191
|
-
return new Promise((resolve, reject) => {
|
|
192
|
-
const useHttps = shouldUseHttps(baseUrl);
|
|
193
|
-
const protocol = useHttps ? https : http;
|
|
194
|
-
const scheme = useHttps ? 'https' : 'http';
|
|
195
|
-
|
|
196
|
-
const url = `${scheme}://${baseUrl}${RAW_PATH}${fullPath}`;
|
|
197
|
-
|
|
198
|
-
protocol
|
|
199
|
-
.get(url, (res) => {
|
|
200
|
-
if (res.statusCode === 200) {
|
|
201
|
-
let data = '';
|
|
202
|
-
res.on('data', (chunk) => {
|
|
203
|
-
data += chunk;
|
|
204
|
-
});
|
|
205
|
-
res.on('end', () => resolve(data));
|
|
206
|
-
} else if (res.statusCode === 404) {
|
|
207
|
-
reject(new Error(`Command '${fullPath}' not found (404)`));
|
|
208
|
-
} else if (useHttps && (res.statusCode === 500 || res.statusCode >= 400)) {
|
|
209
|
-
if (canFallbackToHttp(baseUrl)) {
|
|
210
|
-
console.log(`HTTPS failed (${res.statusCode}), trying HTTP...`);
|
|
211
|
-
downloadWithHttp(baseUrl, fullPath)
|
|
212
|
-
.then(resolve)
|
|
213
|
-
.catch(reject);
|
|
214
|
-
} else {
|
|
215
|
-
reject(new Error(`HTTPS failed (${res.statusCode}) and HTTP fallback is disabled`));
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
219
|
-
}
|
|
220
|
-
})
|
|
221
|
-
.on('error', (err) => {
|
|
222
|
-
if (useHttps) {
|
|
223
|
-
if (canFallbackToHttp(baseUrl)) {
|
|
224
|
-
console.log(`HTTPS failed (${err.message}), trying HTTP...`);
|
|
225
|
-
downloadWithHttp(baseUrl, fullPath)
|
|
226
|
-
.then(resolve)
|
|
227
|
-
.catch(reject);
|
|
228
|
-
} else {
|
|
229
|
-
reject(err);
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
reject(err);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function downloadWithHttp(baseUrl, fullPath) {
|
|
239
|
-
return new Promise((resolve, reject) => {
|
|
240
|
-
const url = `http://${baseUrl}${RAW_PATH}${fullPath}`;
|
|
241
|
-
|
|
242
|
-
console.log(`Trying HTTP: ${url}`);
|
|
243
|
-
|
|
244
|
-
http
|
|
245
|
-
.get(url, (res) => {
|
|
246
|
-
if (res.statusCode === 200) {
|
|
247
|
-
let data = '';
|
|
248
|
-
res.on('data', (chunk) => {
|
|
249
|
-
data += chunk;
|
|
250
|
-
});
|
|
251
|
-
res.on('end', () => resolve(data));
|
|
252
|
-
} else if (res.statusCode === 404) {
|
|
253
|
-
reject(new Error(`Command '${fullPath}' not found (404)`));
|
|
254
|
-
} else {
|
|
255
|
-
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
256
|
-
}
|
|
257
|
-
})
|
|
258
|
-
.on('error', reject);
|
|
259
|
-
});
|
|
482
|
+
return (
|
|
483
|
+
String(identifier || '')
|
|
484
|
+
.normalize('NFKD')
|
|
485
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
486
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, '')
|
|
487
|
+
.replace(/[<>:"/\\|?*]/g, '')
|
|
488
|
+
.replace(/\.\./g, '')
|
|
489
|
+
.replace(/[/\\]+/g, '-')
|
|
490
|
+
.replace(/^\.+/g, '')
|
|
491
|
+
.replace(/\.+$/g, '')
|
|
492
|
+
.replace(/^-+|-+$/g, '')
|
|
493
|
+
.replace(/-+/g, '-')
|
|
494
|
+
.toLowerCase() || 'unknown'
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function getAuthHeaders() {
|
|
499
|
+
const token = getAuthToken();
|
|
500
|
+
if (token) {
|
|
501
|
+
return { Authorization: `Bearer ${token}` };
|
|
502
|
+
}
|
|
503
|
+
return {};
|
|
260
504
|
}
|
|
261
505
|
|
|
262
506
|
function downloadStack(baseUrl, stackId) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
const useHttps = shouldUseHttps(baseUrl);
|
|
509
|
+
const protocol = useHttps ? https : http;
|
|
510
|
+
const scheme = useHttps ? 'https' : 'http';
|
|
511
|
+
const url = `${scheme}://${baseUrl}${STACK_API_PREFIX}${stackId}${STACK_API_SUFFIX}`;
|
|
512
|
+
|
|
513
|
+
const urlObj = new URL(url);
|
|
514
|
+
const options = {
|
|
515
|
+
hostname: urlObj.hostname,
|
|
516
|
+
port: urlObj.port || (useHttps ? 443 : 80),
|
|
517
|
+
path: urlObj.pathname,
|
|
518
|
+
method: 'GET',
|
|
519
|
+
headers: getAuthHeaders(),
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
protocol
|
|
523
|
+
.get(options, (res) => {
|
|
524
|
+
if (res.statusCode === 200) {
|
|
525
|
+
let data = '';
|
|
526
|
+
res.on('data', (chunk) => {
|
|
527
|
+
data += chunk;
|
|
528
|
+
});
|
|
529
|
+
res.on('end', () => {
|
|
530
|
+
try {
|
|
531
|
+
const parsed = JSON.parse(data);
|
|
532
|
+
resolve(parsed);
|
|
533
|
+
} catch (parseError) {
|
|
534
|
+
reject(new Error('Received invalid JSON while downloading stack'));
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
|
538
|
+
reject(
|
|
539
|
+
new Error(
|
|
540
|
+
'This stack is private. Please run: npx @impulselab/directory login'
|
|
541
|
+
)
|
|
542
|
+
);
|
|
543
|
+
} else if (res.statusCode === 404) {
|
|
544
|
+
reject(new Error(`Stack '${stackId}' not found (404)`));
|
|
545
|
+
} else if (useHttps && (res.statusCode === 500 || res.statusCode >= 400)) {
|
|
546
|
+
if (canFallbackToHttp(baseUrl)) {
|
|
547
|
+
console.log(`HTTPS failed (${res.statusCode}), trying HTTP...`);
|
|
548
|
+
downloadStackWithHttp(baseUrl, stackId).then(resolve).catch(reject);
|
|
549
|
+
} else {
|
|
550
|
+
reject(
|
|
551
|
+
new Error(
|
|
552
|
+
`HTTPS failed (${res.statusCode}) and HTTP fallback is disabled`
|
|
553
|
+
)
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
.on('error', (err) => {
|
|
561
|
+
if (useHttps) {
|
|
562
|
+
if (canFallbackToHttp(baseUrl)) {
|
|
563
|
+
console.log(`HTTPS failed (${err.message}), trying HTTP...`);
|
|
564
|
+
downloadStackWithHttp(baseUrl, stackId).then(resolve).catch(reject);
|
|
565
|
+
} else {
|
|
566
|
+
reject(err);
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
reject(err);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
});
|
|
314
573
|
}
|
|
315
574
|
|
|
316
575
|
function downloadStackWithHttp(baseUrl, stackId) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
576
|
+
return new Promise((resolve, reject) => {
|
|
577
|
+
const url = `http://${baseUrl}${STACK_API_PREFIX}${stackId}${STACK_API_SUFFIX}`;
|
|
578
|
+
const urlObj = new URL(url);
|
|
579
|
+
|
|
580
|
+
console.log(`Trying HTTP: ${url}`);
|
|
581
|
+
|
|
582
|
+
const options = {
|
|
583
|
+
hostname: urlObj.hostname,
|
|
584
|
+
port: urlObj.port || 80,
|
|
585
|
+
path: urlObj.pathname,
|
|
586
|
+
method: 'GET',
|
|
587
|
+
headers: getAuthHeaders(),
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
http
|
|
591
|
+
.get(options, (res) => {
|
|
592
|
+
if (res.statusCode === 200) {
|
|
593
|
+
let data = '';
|
|
594
|
+
res.on('data', (chunk) => {
|
|
595
|
+
data += chunk;
|
|
596
|
+
});
|
|
597
|
+
res.on('end', () => {
|
|
598
|
+
try {
|
|
599
|
+
const parsed = JSON.parse(data);
|
|
600
|
+
resolve(parsed);
|
|
601
|
+
} catch (parseError) {
|
|
602
|
+
reject(new Error('Received invalid JSON while downloading stack'));
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
|
606
|
+
reject(
|
|
607
|
+
new Error(
|
|
608
|
+
'This stack is private. Please run: npx @impulselab/directory login'
|
|
609
|
+
)
|
|
610
|
+
);
|
|
611
|
+
} else if (res.statusCode === 404) {
|
|
612
|
+
reject(new Error(`Stack '${stackId}' not found (404)`));
|
|
613
|
+
} else {
|
|
614
|
+
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
615
|
+
}
|
|
616
|
+
})
|
|
617
|
+
.on('error', reject);
|
|
618
|
+
});
|
|
345
619
|
}
|
|
346
620
|
|
|
347
621
|
function downloadRawWithHeaders(baseUrl, fullPath) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
622
|
+
return new Promise((resolve, reject) => {
|
|
623
|
+
const useHttps = shouldUseHttps(baseUrl);
|
|
624
|
+
const protocol = useHttps ? https : http;
|
|
625
|
+
const scheme = useHttps ? 'https' : 'http';
|
|
626
|
+
const url = `${scheme}://${baseUrl}${RAW_PATH}${fullPath}`;
|
|
627
|
+
|
|
628
|
+
const urlObj = new URL(url);
|
|
629
|
+
const options = {
|
|
630
|
+
hostname: urlObj.hostname,
|
|
631
|
+
port: urlObj.port || (useHttps ? 443 : 80),
|
|
632
|
+
path: urlObj.pathname,
|
|
633
|
+
method: 'GET',
|
|
634
|
+
headers: getAuthHeaders(),
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const handleOk = (res) => {
|
|
638
|
+
let data = '';
|
|
639
|
+
res.on('data', (chunk) => {
|
|
640
|
+
data += chunk;
|
|
641
|
+
});
|
|
642
|
+
res.on('end', () => resolve({ content: data, headers: res.headers }));
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const handleHttpsFailover = (reason) => {
|
|
646
|
+
if (useHttps && canFallbackToHttp(baseUrl)) {
|
|
647
|
+
console.log(`HTTPS failed (${reason}), trying HTTP...`);
|
|
648
|
+
downloadRawWithHeadersHttp(baseUrl, fullPath).then(resolve).catch(reject);
|
|
649
|
+
} else {
|
|
650
|
+
reject(new Error(typeof reason === 'string' ? reason : reason.message));
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
protocol
|
|
655
|
+
.get(options, (res) => {
|
|
656
|
+
if (res.statusCode === 200) return handleOk(res);
|
|
657
|
+
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
658
|
+
return reject(
|
|
659
|
+
new Error(
|
|
660
|
+
'This prompt is private. Please run: npx @impulselab/directory login'
|
|
661
|
+
)
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
if (res.statusCode === 404)
|
|
665
|
+
return reject(new Error(`Command '${fullPath}' not found (404)`));
|
|
666
|
+
return handleHttpsFailover(`${res.statusCode}`);
|
|
667
|
+
})
|
|
668
|
+
.on('error', handleHttpsFailover);
|
|
669
|
+
});
|
|
377
670
|
}
|
|
378
671
|
|
|
379
672
|
function downloadRawWithHeadersHttp(baseUrl, fullPath) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
673
|
+
return new Promise((resolve, reject) => {
|
|
674
|
+
const url = `http://${baseUrl}${RAW_PATH}${fullPath}`;
|
|
675
|
+
const urlObj = new URL(url);
|
|
676
|
+
|
|
677
|
+
console.log(`Trying HTTP: ${url}`);
|
|
678
|
+
|
|
679
|
+
const options = {
|
|
680
|
+
hostname: urlObj.hostname,
|
|
681
|
+
port: urlObj.port || 80,
|
|
682
|
+
path: urlObj.pathname,
|
|
683
|
+
method: 'GET',
|
|
684
|
+
headers: getAuthHeaders(),
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
http
|
|
688
|
+
.get(options, (res) => {
|
|
689
|
+
if (res.statusCode === 200) {
|
|
690
|
+
let data = '';
|
|
691
|
+
res.on('data', (chunk) => {
|
|
692
|
+
data += chunk;
|
|
693
|
+
});
|
|
694
|
+
res.on('end', () => resolve({ content: data, headers: res.headers }));
|
|
695
|
+
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
|
696
|
+
reject(
|
|
697
|
+
new Error(
|
|
698
|
+
'This prompt is private. Please run: npx @impulselab/directory login'
|
|
699
|
+
)
|
|
700
|
+
);
|
|
701
|
+
} else if (res.statusCode === 404) {
|
|
702
|
+
reject(new Error(`Command '${fullPath}' not found (404)`));
|
|
703
|
+
} else {
|
|
704
|
+
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
|
|
705
|
+
}
|
|
706
|
+
})
|
|
707
|
+
.on('error', reject);
|
|
708
|
+
});
|
|
397
709
|
}
|
|
398
710
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
while (true) {
|
|
403
|
-
const candidate = path.join(currentDir, folderName);
|
|
404
|
-
if (fs.existsSync(candidate)) {
|
|
405
|
-
return candidate;
|
|
406
|
-
}
|
|
711
|
+
// ============================================
|
|
712
|
+
// Directory Resolution
|
|
713
|
+
// ============================================
|
|
407
714
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
715
|
+
function findClosestFolder(folderName) {
|
|
716
|
+
let currentDir = process.cwd();
|
|
717
|
+
|
|
718
|
+
while (true) {
|
|
719
|
+
const candidate = path.join(currentDir, folderName);
|
|
720
|
+
if (fs.existsSync(candidate)) {
|
|
721
|
+
return candidate;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const parentDir = path.dirname(currentDir);
|
|
725
|
+
if (parentDir === currentDir) {
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
currentDir = parentDir;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return null;
|
|
416
732
|
}
|
|
417
733
|
|
|
418
734
|
function resolveClaudeDirectory(subFolder) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
735
|
+
const projectClaudeDir = findClosestFolder('.claude');
|
|
736
|
+
if (projectClaudeDir) {
|
|
737
|
+
return path.join(projectClaudeDir, subFolder);
|
|
738
|
+
}
|
|
739
|
+
return path.join(os.homedir(), '.claude', subFolder);
|
|
424
740
|
}
|
|
425
741
|
|
|
426
742
|
function resolveCursorDirectory(subFolder) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
743
|
+
const projectCursorDir = findClosestFolder('.cursor');
|
|
744
|
+
if (!projectCursorDir) {
|
|
745
|
+
const message =
|
|
746
|
+
'Unable to find a .cursor directory. Run this command from inside a Cursor project.';
|
|
747
|
+
throw new Error(message);
|
|
748
|
+
}
|
|
749
|
+
return path.join(projectCursorDir, subFolder);
|
|
434
750
|
}
|
|
435
751
|
|
|
436
752
|
function ensureDirectoryExists(dirPath) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function checkFileExists(commandsDir, filename) {
|
|
444
|
-
const filePath = path.join(commandsDir, filename);
|
|
445
|
-
return fs.existsSync(filePath);
|
|
753
|
+
if (!fs.existsSync(dirPath)) {
|
|
754
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
755
|
+
console.log(`Created directory: ${dirPath}`);
|
|
756
|
+
}
|
|
446
757
|
}
|
|
447
758
|
|
|
448
|
-
function saveCommand({
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
fullPath,
|
|
453
|
-
target,
|
|
454
|
-
}) {
|
|
455
|
-
const filePath = path.join(directory, filename);
|
|
456
|
-
const relativeDir = path.relative(process.cwd(), directory);
|
|
457
|
-
const displayPath = relativeDir
|
|
458
|
-
? path.join(relativeDir, filename)
|
|
459
|
-
: filename;
|
|
759
|
+
function saveCommand({ directory, filename, content, fullPath, target }) {
|
|
760
|
+
const filePath = path.join(directory, filename);
|
|
761
|
+
const relativeDir = path.relative(process.cwd(), directory);
|
|
762
|
+
const displayPath = relativeDir ? path.join(relativeDir, filename) : filename;
|
|
460
763
|
|
|
461
|
-
|
|
764
|
+
console.log(`📁 Saving command to ${displayPath}`);
|
|
462
765
|
|
|
463
|
-
|
|
766
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
464
767
|
|
|
465
|
-
|
|
466
|
-
|
|
768
|
+
const commandName = filename.replace(path.extname(filename), '');
|
|
769
|
+
logSuccessMessage(target, fullPath, commandName);
|
|
467
770
|
}
|
|
468
771
|
|
|
469
772
|
function logSuccessMessage(target, fullPath, commandName) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
console.log(`✅ '${fullPath}' installed successfully`);
|
|
495
|
-
}
|
|
773
|
+
switch (target) {
|
|
774
|
+
case INSTALLATION_TARGETS.CLAUDE_SLASH:
|
|
775
|
+
console.log(`✅ Command '${fullPath}' installed successfully`);
|
|
776
|
+
console.log(`\n🎉 You can now use: /${commandName}`);
|
|
777
|
+
break;
|
|
778
|
+
case INSTALLATION_TARGETS.CLAUDE_SUB_AGENT:
|
|
779
|
+
console.log(`✅ Agent '${fullPath}' installed successfully`);
|
|
780
|
+
console.log(
|
|
781
|
+
`\n🎉 You can now select '${commandName}' from Claude Desktop's agent menu.`
|
|
782
|
+
);
|
|
783
|
+
break;
|
|
784
|
+
case INSTALLATION_TARGETS.CURSOR_COMMAND:
|
|
785
|
+
console.log(`✅ Cursor command '${fullPath}' installed successfully`);
|
|
786
|
+
console.log(
|
|
787
|
+
`\n🎉 Open Cursor and run '${commandName}' from the command palette.`
|
|
788
|
+
);
|
|
789
|
+
break;
|
|
790
|
+
case INSTALLATION_TARGETS.CURSOR_RULE:
|
|
791
|
+
console.log(`✅ Cursor rule '${fullPath}' installed successfully`);
|
|
792
|
+
console.log(`\n🎉 Cursor will apply '${commandName}' to this project.`);
|
|
793
|
+
break;
|
|
794
|
+
default:
|
|
795
|
+
console.log(`✅ '${fullPath}' installed successfully`);
|
|
796
|
+
}
|
|
496
797
|
}
|
|
497
798
|
|
|
799
|
+
// ============================================
|
|
800
|
+
// Argument Parsing
|
|
801
|
+
// ============================================
|
|
802
|
+
|
|
498
803
|
function parseCommandArgs(args) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
804
|
+
let mode = MODES.COMMAND;
|
|
805
|
+
let identifier = null;
|
|
806
|
+
let target = null;
|
|
807
|
+
let showHelpFlag = false;
|
|
808
|
+
|
|
809
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
810
|
+
const arg = args[i];
|
|
811
|
+
|
|
812
|
+
if (arg === '--help' || arg === '-h') {
|
|
813
|
+
showHelpFlag = true;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (arg.startsWith('--target')) {
|
|
818
|
+
let rawValue = null;
|
|
819
|
+
if (arg.includes('=')) {
|
|
820
|
+
rawValue = arg.split('=')[1];
|
|
821
|
+
} else {
|
|
822
|
+
rawValue = args[i + 1];
|
|
823
|
+
i += 1;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (!rawValue) {
|
|
827
|
+
throw new Error('Missing value for --target option');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const normalizedTarget = rawValue.toLowerCase();
|
|
831
|
+
if (!KNOWN_TARGETS.includes(normalizedTarget)) {
|
|
832
|
+
throw new Error(
|
|
833
|
+
`Unknown target '${rawValue}'. Available targets: ${KNOWN_TARGETS.join(', ')}`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
target = normalizedTarget;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (arg === '-t') {
|
|
842
|
+
const rawValue = args[i + 1];
|
|
843
|
+
i += 1;
|
|
844
|
+
|
|
845
|
+
if (!rawValue) {
|
|
846
|
+
throw new Error('Missing value for -t option');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const normalizedTarget = rawValue.toLowerCase();
|
|
850
|
+
if (!KNOWN_TARGETS.includes(normalizedTarget)) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
`Unknown target '${rawValue}'. Available targets: ${KNOWN_TARGETS.join(', ')}`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
target = normalizedTarget;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Check for auth commands
|
|
861
|
+
if (arg === 'login' && !identifier) {
|
|
862
|
+
mode = MODES.LOGIN;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (arg === 'logout' && !identifier) {
|
|
867
|
+
mode = MODES.LOGOUT;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (arg === 'whoami' && !identifier) {
|
|
872
|
+
mode = MODES.WHOAMI;
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (mode === MODES.COMMAND && arg === 'stack' && !identifier) {
|
|
877
|
+
mode = MODES.STACK;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (!identifier) {
|
|
882
|
+
identifier = arg;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return { mode, identifier, target, showHelp: showHelpFlag };
|
|
569
890
|
}
|
|
570
891
|
|
|
892
|
+
// ============================================
|
|
893
|
+
// Main
|
|
894
|
+
// ============================================
|
|
895
|
+
|
|
571
896
|
async function main() {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
897
|
+
const args = process.argv.slice(2);
|
|
898
|
+
|
|
899
|
+
try {
|
|
900
|
+
const { mode, identifier, target, showHelp: helpRequested } = parseCommandArgs(args);
|
|
901
|
+
|
|
902
|
+
if (helpRequested || args.length === 0) {
|
|
903
|
+
showHelp();
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const baseUrl = normalizeBaseHost(
|
|
908
|
+
process.env.IMPULSE_BASE_URL || DEFAULT_BASE_URL
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
// Handle auth commands
|
|
912
|
+
if (mode === MODES.LOGIN) {
|
|
913
|
+
await doLogin(baseUrl);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (mode === MODES.LOGOUT) {
|
|
918
|
+
await doLogout();
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (mode === MODES.WHOAMI) {
|
|
923
|
+
await doWhoami(baseUrl);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (mode === MODES.COMMAND) {
|
|
928
|
+
if (!identifier) {
|
|
929
|
+
console.error('❌ Error: Missing command path');
|
|
930
|
+
showHelp();
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (!identifier.includes('/')) {
|
|
935
|
+
console.error('❌ Error: Path must include user/command format');
|
|
936
|
+
showHelp();
|
|
937
|
+
process.exit(1);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const resolvedTarget = target || DEFAULT_TARGET;
|
|
941
|
+
const targetConfig = TARGET_CONFIG[resolvedTarget];
|
|
942
|
+
|
|
943
|
+
if (!targetConfig) {
|
|
944
|
+
console.error(`❌ Error: Unsupported target '${resolvedTarget}'`);
|
|
945
|
+
process.exit(1);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const directory = targetConfig.resolveDirectory();
|
|
949
|
+
|
|
950
|
+
ensureDirectoryExists(directory);
|
|
951
|
+
|
|
952
|
+
const commandNameFromPath = identifier.split('/').pop();
|
|
953
|
+
const { content, headers } = await downloadRawWithHeaders(baseUrl, identifier);
|
|
954
|
+
const titleHeader = headers['x-prompt-title'];
|
|
955
|
+
const title = Array.isArray(titleHeader) ? titleHeader[0] : titleHeader;
|
|
956
|
+
|
|
957
|
+
const baseNameCandidate = title
|
|
958
|
+
? toKebabCase(title)
|
|
959
|
+
: stripTrailingIdSegment(commandNameFromPath);
|
|
960
|
+
const finalBaseName = sanitizeIdentifierForFilename(baseNameCandidate);
|
|
961
|
+
|
|
962
|
+
const filename = `${finalBaseName}${targetConfig.extension}`;
|
|
963
|
+
|
|
964
|
+
saveCommand({
|
|
965
|
+
directory,
|
|
966
|
+
filename,
|
|
967
|
+
content,
|
|
968
|
+
fullPath: identifier,
|
|
969
|
+
target: resolvedTarget,
|
|
970
|
+
});
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (!identifier) {
|
|
975
|
+
console.error('❌ Error: Missing stack identifier');
|
|
976
|
+
showHelp();
|
|
977
|
+
process.exit(1);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const stackPayload = await downloadStack(baseUrl, identifier);
|
|
981
|
+
const stack = stackPayload.stack || {};
|
|
982
|
+
const prompts = Array.isArray(stackPayload.prompts) ? stackPayload.prompts : [];
|
|
983
|
+
|
|
984
|
+
console.log(
|
|
985
|
+
`📦 Installing stack '${stack.title || identifier}' (${prompts.length} prompt${
|
|
986
|
+
prompts.length === 1 ? '' : 's'
|
|
987
|
+
})`
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
let installed = 0;
|
|
991
|
+
let skipped = 0;
|
|
992
|
+
|
|
993
|
+
for (let index = 0; index < prompts.length; index += 1) {
|
|
994
|
+
const prompt = prompts[index];
|
|
995
|
+
const commandPath =
|
|
996
|
+
prompt.path || prompt.slug || prompt.id || `prompt-${index + 1}`;
|
|
997
|
+
const baseName = prompt.title
|
|
998
|
+
? toKebabCase(prompt.title)
|
|
999
|
+
: stripTrailingIdSegment(prompt.slug || `prompt-${index + 1}`);
|
|
1000
|
+
|
|
1001
|
+
const resolvedTarget = target || prompt.target || DEFAULT_TARGET;
|
|
1002
|
+
const targetConfig = TARGET_CONFIG[resolvedTarget];
|
|
1003
|
+
if (!targetConfig) {
|
|
1004
|
+
console.warn(
|
|
1005
|
+
`⚠️ Skipping prompt '${commandPath}' (unsupported target '${resolvedTarget}')`
|
|
1006
|
+
);
|
|
1007
|
+
skipped += 1;
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const directory = targetConfig.resolveDirectory();
|
|
1012
|
+
ensureDirectoryExists(directory);
|
|
1013
|
+
|
|
1014
|
+
const sanitizedBaseName = sanitizeIdentifierForFilename(baseName);
|
|
1015
|
+
const filename = `${sanitizedBaseName}${targetConfig.extension}`;
|
|
1016
|
+
|
|
1017
|
+
if (!prompt.markdown) {
|
|
1018
|
+
console.warn(`⚠️ Skipping prompt '${commandPath}' (no content available)`);
|
|
1019
|
+
skipped += 1;
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
saveCommand({
|
|
1024
|
+
directory,
|
|
1025
|
+
filename,
|
|
1026
|
+
content: prompt.markdown,
|
|
1027
|
+
fullPath: commandPath,
|
|
1028
|
+
target: resolvedTarget,
|
|
1029
|
+
});
|
|
1030
|
+
installed += 1;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
console.log(
|
|
1034
|
+
`🎉 Installed ${installed} prompt${installed === 1 ? '' : 's'} from stack '${
|
|
1035
|
+
stack.title || identifier
|
|
1036
|
+
}'${skipped ? ` (skipped ${skipped})` : ''}`
|
|
1037
|
+
);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
console.error(`❌ Error: ${error.message}`);
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
706
1042
|
}
|
|
707
1043
|
|
|
708
1044
|
if (require.main === module) {
|
|
709
|
-
|
|
1045
|
+
main();
|
|
710
1046
|
}
|
|
711
1047
|
|
|
712
1048
|
module.exports = { main };
|