@quadient/skillshare 1.6.2 → 1.8.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/README.md +37 -0
- package/dist/cli-args.js +86 -0
- package/dist/help.js +167 -0
- package/dist/index.js +294 -126
- package/dist/install-files.js +111 -0
- package/dist/teams-auth.js +304 -0
- package/dist/teams-watch.js +45 -0
- package/dist/teams.js +142 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { assertCredentialDestination, runTeamsCommand } from './teams.js';
|
|
2
|
+
import { formatUnknownCommand, parseCliArgs } from './cli-args.js';
|
|
3
|
+
import { findCommand, readCliVersion, renderCommandHelp, renderUsage } from './help.js';
|
|
4
|
+
import { inspectInstallTarget, installFilesAtomically } from './install-files.js';
|
|
1
5
|
import { preparePageFiles } from './pages.js';
|
|
2
6
|
import { runPageDev } from './pages-dev.js';
|
|
3
7
|
import { createServer } from 'node:http';
|
|
@@ -5,9 +9,9 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
5
9
|
import { spawn } from 'node:child_process';
|
|
6
10
|
import { existsSync } from 'node:fs';
|
|
7
11
|
import { mkdir, open, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
8
|
-
import { basename,
|
|
12
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
9
13
|
import { homedir } from 'node:os';
|
|
10
|
-
import { cancel, confirm, intro, isCancel, multiselect, outro, progress, select, spinner, text } from '@clack/prompts';
|
|
14
|
+
import { cancel, confirm, intro, isCancel, log, multiselect, outro, progress, select, spinner, text } from '@clack/prompts';
|
|
11
15
|
const DEFAULT_URL = 'https://skillshare.quadientcloudpreprod.eu';
|
|
12
16
|
// Records without a registry URL were created against UAT before the preprod migration.
|
|
13
17
|
const LEGACY_UAT_URL = 'https://skillshare.inspireclouduat.net';
|
|
@@ -67,36 +71,7 @@ const AGENTS = [
|
|
|
67
71
|
{ value: 'pochi', label: 'Pochi', projectPath: '.pochi/skills', globalPath: '.pochi/skills' },
|
|
68
72
|
{ value: 'adal', label: 'AdaL', projectPath: '.adal/skills', globalPath: '.adal/skills' },
|
|
69
73
|
];
|
|
70
|
-
const
|
|
71
|
-
skillshare <command> [options]
|
|
72
|
-
|
|
73
|
-
Commands:
|
|
74
|
-
login [local]
|
|
75
|
-
logout
|
|
76
|
-
whoami
|
|
77
|
-
list
|
|
78
|
-
show <skillId> [version]
|
|
79
|
-
upload <directory> --version <version> [--skill-id <id>] [--name <name> --description <desc>]
|
|
80
|
-
install
|
|
81
|
-
install <skillId|skillName> [version]
|
|
82
|
-
install <skillId|skillName> [version] --target <directory>
|
|
83
|
-
install --skill-version-id <id> --target <directory>
|
|
84
|
-
update [skillId|skillName] [--target <directory>] [--dry-run]
|
|
85
|
-
pages list
|
|
86
|
-
pages show <slug>
|
|
87
|
-
pages upload <directory> [--slug <slug>]
|
|
88
|
-
pages dev [directory] [--slug <slug>] [--port <port>]
|
|
89
|
-
[--build <command> --watch-dir <sources>] [--celld <executable>]
|
|
90
|
-
[--user-id <id>] [--user-name <name>] [--user-email <email>]
|
|
91
|
-
git configure [local]
|
|
92
|
-
git unconfigure [local]
|
|
93
|
-
|
|
94
|
-
Options:
|
|
95
|
-
--token <token> Bearer token for API requests (overrides SKILLSHARE_TOKEN)
|
|
96
|
-
|
|
97
|
-
Default server: ${DEFAULT_URL}
|
|
98
|
-
A stored session keeps its server; run "skillshare login" to switch to the default.
|
|
99
|
-
`;
|
|
74
|
+
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
100
75
|
async function main() {
|
|
101
76
|
const args = process.argv.slice(2);
|
|
102
77
|
const command = args[0];
|
|
@@ -104,24 +79,52 @@ async function main() {
|
|
|
104
79
|
await runInteractive();
|
|
105
80
|
return;
|
|
106
81
|
}
|
|
107
|
-
if (command === '--
|
|
108
|
-
console.log(
|
|
109
|
-
|
|
82
|
+
if (command === '--version' || command === '-v') {
|
|
83
|
+
console.log(readCliVersion());
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
87
|
+
const topic = command === 'help' ? findCommand(args[1]) : undefined;
|
|
88
|
+
if (command === 'help' && args[1] && !topic) {
|
|
89
|
+
throw new Error(formatUnknownCommand(args[1]));
|
|
90
|
+
}
|
|
91
|
+
console.log(topic ? renderCommandHelp(topic) : renderUsage(DEFAULT_URL));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const commandSpec = findCommand(command);
|
|
95
|
+
if (!commandSpec) {
|
|
96
|
+
throw new Error(formatUnknownCommand(command));
|
|
97
|
+
}
|
|
98
|
+
// pages dev validates its own options.
|
|
99
|
+
const isPageDev = (command === 'pages' || command === 'page') && args[1] === 'dev';
|
|
100
|
+
const options = await parseOptions(args.slice(1), isPageDev ? undefined : command);
|
|
101
|
+
if (isFlagEnabled(options.flags, 'help')) {
|
|
102
|
+
console.log(renderCommandHelp(commandSpec));
|
|
103
|
+
return;
|
|
110
104
|
}
|
|
111
|
-
const options = await parseOptions(args.slice(1));
|
|
112
105
|
if ((command === 'pages' || command === 'page') && options.positionals[0] === 'dev') {
|
|
113
106
|
await runPageDev(options.positionals[1] ?? '.', options.flags);
|
|
114
107
|
return;
|
|
115
108
|
}
|
|
116
109
|
switch (command) {
|
|
117
110
|
case 'login': {
|
|
118
|
-
|
|
111
|
+
const baseUrl = options.flags.server ?? (options.positionals[0] === 'local' ? LOCAL_URL : DEFAULT_URL);
|
|
112
|
+
assertCredentialDestination(baseUrl);
|
|
113
|
+
await login(baseUrl);
|
|
119
114
|
return;
|
|
120
115
|
}
|
|
121
116
|
case 'logout': {
|
|
122
117
|
await logoutSession();
|
|
123
118
|
return;
|
|
124
119
|
}
|
|
120
|
+
case 'teams': {
|
|
121
|
+
const baseUrl = options.flags.server ?? (options.positionals[1] === 'local' ? LOCAL_URL : await resolveActiveBaseUrl());
|
|
122
|
+
assertCredentialDestination(baseUrl);
|
|
123
|
+
if (!options.token && !await loadSession(baseUrl))
|
|
124
|
+
await login(baseUrl);
|
|
125
|
+
await runTeamsCommand(await createClient(baseUrl, options.token), options.positionals, options.flags, APP_DIR);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
125
128
|
case 'git': {
|
|
126
129
|
const action = options.positionals[0];
|
|
127
130
|
const baseUrl = options.positionals[1] === 'local' ? LOCAL_URL : DEFAULT_URL;
|
|
@@ -203,6 +206,7 @@ async function main() {
|
|
|
203
206
|
const skillRef = options.positionals[0];
|
|
204
207
|
const version = options.positionals[1];
|
|
205
208
|
const target = options.flags.target;
|
|
209
|
+
const force = isFlagEnabled(options.flags, 'force');
|
|
206
210
|
if (skillVersionId) {
|
|
207
211
|
if (!target) {
|
|
208
212
|
throw new Error('install with --skill-version-id requires --target <directory>');
|
|
@@ -213,6 +217,7 @@ async function main() {
|
|
|
213
217
|
}
|
|
214
218
|
const detail = await client.get(`/api/skill-versions/${parsedSkillVersionId}`);
|
|
215
219
|
const skill = await client.get(`/api/skills/${detail.skillId}`);
|
|
220
|
+
await ensureTargetsWritable(client.baseUrl, [target], { force });
|
|
216
221
|
await client.post(`/api/skills/${detail.skillId}/versions/${detail.version}/downloads`);
|
|
217
222
|
const installProgress = createInstallProgressReporter({
|
|
218
223
|
skillName: skill.name,
|
|
@@ -242,6 +247,7 @@ async function main() {
|
|
|
242
247
|
await runInteractiveInstall(client, {
|
|
243
248
|
skillRef,
|
|
244
249
|
version,
|
|
250
|
+
force,
|
|
245
251
|
});
|
|
246
252
|
return;
|
|
247
253
|
}
|
|
@@ -255,6 +261,7 @@ async function main() {
|
|
|
255
261
|
throw new Error(`Skill '${skill.name}' does not have a published version.`);
|
|
256
262
|
}
|
|
257
263
|
const detail = await client.get(`/api/skills/${skill.id}/versions/${effectiveVersion}`);
|
|
264
|
+
await ensureTargetsWritable(client.baseUrl, [target], { force });
|
|
258
265
|
await client.post(`/api/skills/${skill.id}/versions/${effectiveVersion}/downloads`);
|
|
259
266
|
const installProgress = createInstallProgressReporter({
|
|
260
267
|
skillName: skill.name,
|
|
@@ -285,6 +292,7 @@ async function main() {
|
|
|
285
292
|
skillRef: options.positionals[0],
|
|
286
293
|
target: options.flags.target,
|
|
287
294
|
dryRun: isFlagEnabled(options.flags, 'dry-run'),
|
|
295
|
+
force: isFlagEnabled(options.flags, 'force'),
|
|
288
296
|
});
|
|
289
297
|
return;
|
|
290
298
|
}
|
|
@@ -331,23 +339,22 @@ async function main() {
|
|
|
331
339
|
return;
|
|
332
340
|
}
|
|
333
341
|
default:
|
|
334
|
-
throw new Error(
|
|
342
|
+
throw new Error(formatUnknownCommand(command));
|
|
335
343
|
}
|
|
336
344
|
}
|
|
337
345
|
async function runInteractive() {
|
|
338
|
-
intro(
|
|
346
|
+
intro(`Skillshare ${readCliVersion()}`);
|
|
339
347
|
const baseUrl = await resolveActiveBaseUrl();
|
|
340
|
-
await ensureInteractiveLogin(baseUrl);
|
|
348
|
+
const user = await ensureInteractiveLogin(baseUrl);
|
|
341
349
|
const client = await createClient(baseUrl);
|
|
342
|
-
|
|
350
|
+
log.info(`Signed in as ${user.displayName}${user.email ? ` <${user.email}>` : ''} · ${new URL(client.baseUrl).host}`);
|
|
351
|
+
const pendingInstalledUpdates = await checkInstalledSkillsForUpdates(client);
|
|
343
352
|
if (pendingInstalledUpdates.length > 0) {
|
|
344
353
|
const shouldUpdateInstalled = await promptConfirm(pendingInstalledUpdates.length === 1
|
|
345
354
|
? `There is 1 installed skill with a newer version available. Update it now?`
|
|
346
355
|
: `There are ${pendingInstalledUpdates.length} installed skills with newer versions available. Update all of them now?`, true);
|
|
347
356
|
if (shouldUpdateInstalled) {
|
|
348
357
|
await updateInstalledSkills(client, { candidates: pendingInstalledUpdates });
|
|
349
|
-
outro('Done.');
|
|
350
|
-
return;
|
|
351
358
|
}
|
|
352
359
|
}
|
|
353
360
|
if (existsSync(join(process.cwd(), SKILL_FILE_NAME))) {
|
|
@@ -367,12 +374,17 @@ async function runInteractive() {
|
|
|
367
374
|
{ value: 'install', label: 'Install a skill', hint: 'Browse registry and install to agents' },
|
|
368
375
|
{ value: 'update', label: 'Update installed skills', hint: 'Refresh recorded installs to the latest registry versions' },
|
|
369
376
|
{ value: 'upload', label: 'Upload a skill', hint: 'Create a new skill or publish a version' },
|
|
377
|
+
{ value: 'exit', label: 'Exit' },
|
|
370
378
|
],
|
|
371
379
|
});
|
|
372
380
|
if (isCancel(action)) {
|
|
373
381
|
cancel('Cancelled.');
|
|
374
382
|
process.exit(0);
|
|
375
383
|
}
|
|
384
|
+
if (action === 'exit') {
|
|
385
|
+
outro('Bye.');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
376
388
|
if (action === 'install') {
|
|
377
389
|
await runInteractiveInstall(client);
|
|
378
390
|
}
|
|
@@ -384,22 +396,53 @@ async function runInteractive() {
|
|
|
384
396
|
}
|
|
385
397
|
outro('Done.');
|
|
386
398
|
}
|
|
399
|
+
async function checkInstalledSkillsForUpdates(client) {
|
|
400
|
+
const records = (await readInstalledSkillRecords()).filter((record) => record.baseUrl === client.baseUrl);
|
|
401
|
+
if (records.length === 0) {
|
|
402
|
+
return [];
|
|
403
|
+
}
|
|
404
|
+
const status = spinner();
|
|
405
|
+
status.start(`Checking ${records.length} installed skill${records.length === 1 ? '' : 's'} for updates`);
|
|
406
|
+
const problems = [];
|
|
407
|
+
try {
|
|
408
|
+
const candidates = await collectInstalledSkillUpdateCandidates(client, {
|
|
409
|
+
records,
|
|
410
|
+
onSkipped(skip) {
|
|
411
|
+
if (skip.reason === 'removed' || skip.reason === 'error') {
|
|
412
|
+
problems.push(skip.message);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
status.stop(candidates.length > 0
|
|
417
|
+
? `${candidates.length} installed skill${candidates.length === 1 ? ' has' : 's have'} a newer version`
|
|
418
|
+
: 'Installed skills are up to date');
|
|
419
|
+
if (problems.length > 0) {
|
|
420
|
+
log.warn(problems.join('\n'));
|
|
421
|
+
}
|
|
422
|
+
return candidates;
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
// The update check is optional; never let it block the main menu.
|
|
426
|
+
status.stop('Could not check installed skills for updates');
|
|
427
|
+
log.warn(error instanceof Error ? error.message : String(error));
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
}
|
|
387
431
|
async function ensureInteractiveLogin(baseUrl) {
|
|
388
432
|
const session = await loadSession(baseUrl);
|
|
389
|
-
if (!session) {
|
|
390
|
-
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
if (new Date(session.refreshTokenExpiresAt).getTime() <= Date.now()) {
|
|
394
|
-
await login(baseUrl);
|
|
395
|
-
return;
|
|
433
|
+
if (!session || new Date(session.refreshTokenExpiresAt).getTime() <= Date.now()) {
|
|
434
|
+
return login(baseUrl);
|
|
396
435
|
}
|
|
397
436
|
try {
|
|
398
437
|
const client = await createClient(baseUrl);
|
|
399
|
-
await client.get('/api/auth/me');
|
|
438
|
+
return await client.get('/api/auth/me');
|
|
400
439
|
}
|
|
401
|
-
catch {
|
|
402
|
-
|
|
440
|
+
catch (error) {
|
|
441
|
+
// Offline is not an expired session; signing in again would not help.
|
|
442
|
+
if (error instanceof NetworkError) {
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
return login(baseUrl);
|
|
403
446
|
}
|
|
404
447
|
}
|
|
405
448
|
async function runInteractiveInstall(client, options) {
|
|
@@ -420,9 +463,13 @@ async function runInteractiveInstall(client, options) {
|
|
|
420
463
|
throw new Error('Select at least one agent.');
|
|
421
464
|
}
|
|
422
465
|
const versionDetail = await client.get(`/api/skills/${skill.id}/versions/${selectedVersion}`);
|
|
423
|
-
await client.post(`/api/skills/${skill.id}/versions/${selectedVersion}/downloads`);
|
|
424
466
|
const installName = createInstalledSkillDirectoryName(skill);
|
|
425
467
|
const installTargets = resolveInstallTargets(selectedAgents, scope, installName);
|
|
468
|
+
await ensureTargetsWritable(client.baseUrl, installTargets.map((target) => target.targetRoot), {
|
|
469
|
+
force: options?.force,
|
|
470
|
+
interactive: true,
|
|
471
|
+
});
|
|
472
|
+
await client.post(`/api/skills/${skill.id}/versions/${selectedVersion}/downloads`);
|
|
426
473
|
const installProgress = createInstallProgressReporter({
|
|
427
474
|
skillName: skill.name,
|
|
428
475
|
version: selectedVersion,
|
|
@@ -446,7 +493,11 @@ async function runInteractiveInstall(client, options) {
|
|
|
446
493
|
installProgress.clear();
|
|
447
494
|
throw error;
|
|
448
495
|
}
|
|
449
|
-
|
|
496
|
+
log.success([
|
|
497
|
+
`Installed ${skill.name}@${selectedVersion} for ${selectedAgents.length} agent${selectedAgents.length === 1 ? '' : 's'}:`,
|
|
498
|
+
...installTargets.map((target) => ` ${formatInstallTargetRoot(target.targetRoot)}`),
|
|
499
|
+
'Restart running agents to pick up the skill.',
|
|
500
|
+
].join('\n'));
|
|
450
501
|
}
|
|
451
502
|
async function runInteractiveUpload(client, initialDirectory) {
|
|
452
503
|
const directory = initialDirectory
|
|
@@ -504,6 +555,7 @@ async function updateInstalledSkills(client, options) {
|
|
|
504
555
|
const targetFilter = normalizeText(options?.target);
|
|
505
556
|
const normalizedTargetFilter = targetFilter ? resolve(targetFilter) : null;
|
|
506
557
|
const dryRun = options?.dryRun ?? false;
|
|
558
|
+
const force = options?.force ?? false;
|
|
507
559
|
let installedRecords = (await readInstalledSkillRecords())
|
|
508
560
|
.filter((record) => record.baseUrl === client.baseUrl);
|
|
509
561
|
if (skillRef) {
|
|
@@ -527,11 +579,11 @@ async function updateInstalledSkills(client, options) {
|
|
|
527
579
|
const candidates = options?.candidates
|
|
528
580
|
?? await collectInstalledSkillUpdateCandidates(client, {
|
|
529
581
|
records: installedRecords,
|
|
530
|
-
onSkipped(
|
|
531
|
-
console.log(message);
|
|
582
|
+
onSkipped(skip) {
|
|
583
|
+
console.log(skip.message);
|
|
532
584
|
},
|
|
533
585
|
});
|
|
534
|
-
|
|
586
|
+
let skippedCount = installedRecords.length - candidates.length;
|
|
535
587
|
if (candidates.length === 0) {
|
|
536
588
|
if (dryRun) {
|
|
537
589
|
console.log(`Dry run finished. Would update 0, skipped ${skippedCount}.`);
|
|
@@ -545,9 +597,16 @@ async function updateInstalledSkills(client, options) {
|
|
|
545
597
|
let plannedCount = 0;
|
|
546
598
|
for (const candidate of candidates) {
|
|
547
599
|
const { record, detail, latestVersion } = candidate;
|
|
600
|
+
const targetState = force ? null : await inspectInstallTarget(record.targetRoot, record);
|
|
601
|
+
if (targetState?.state === 'modified') {
|
|
602
|
+
skippedCount += 1;
|
|
603
|
+
console.log(`Skipping ${detail.name} in ${formatInstallTargetRoot(record.targetRoot)}: local changes in ${formatChangedPaths(targetState.paths)}. `
|
|
604
|
+
+ `Run "skillshare update ${record.skillId} --target ${formatCliPath(record.targetRoot)} --force" to overwrite them.`);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
548
607
|
if (dryRun) {
|
|
549
608
|
plannedCount += 1;
|
|
550
|
-
console.log(`Would update ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${record.targetRoot}`);
|
|
609
|
+
console.log(`Would update ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${formatInstallTargetRoot(record.targetRoot)}`);
|
|
551
610
|
continue;
|
|
552
611
|
}
|
|
553
612
|
const versionDetail = await client.get(`/api/skills/${record.skillId}/versions/${latestVersion}`);
|
|
@@ -574,7 +633,7 @@ async function updateInstalledSkills(client, options) {
|
|
|
574
633
|
throw error;
|
|
575
634
|
}
|
|
576
635
|
updatedCount += 1;
|
|
577
|
-
console.log(`Updated ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${record.targetRoot}`);
|
|
636
|
+
console.log(`Updated ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${formatInstallTargetRoot(record.targetRoot)}`);
|
|
578
637
|
}
|
|
579
638
|
if (dryRun) {
|
|
580
639
|
console.log(`Dry run finished. Would update ${plannedCount}, skipped ${skippedCount}.`);
|
|
@@ -585,30 +644,59 @@ async function updateInstalledSkills(client, options) {
|
|
|
585
644
|
async function collectInstalledSkillUpdateCandidates(client, options) {
|
|
586
645
|
const records = options?.records
|
|
587
646
|
?? (await readInstalledSkillRecords()).filter((record) => record.baseUrl === client.baseUrl);
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
647
|
+
const details = new Map();
|
|
648
|
+
// Each record is checked independently, so one removed skill or failed request does not hide the others.
|
|
649
|
+
const results = await mapWithConcurrency(records, 6, async (record) => {
|
|
650
|
+
const skillId = record.skillId;
|
|
651
|
+
if (skillId == null) {
|
|
652
|
+
return { record, reason: 'no-id', message: `Skipping ${record.skillName}: installed record in ${record.targetRoot} does not contain a skill id.` };
|
|
653
|
+
}
|
|
654
|
+
let detail;
|
|
655
|
+
try {
|
|
656
|
+
if (!details.has(skillId)) {
|
|
657
|
+
details.set(skillId, client.get(`/api/skills/${skillId}`));
|
|
658
|
+
}
|
|
659
|
+
detail = await details.get(skillId);
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
663
|
+
return { record, reason: 'removed', message: `${record.skillName || `Skill ${skillId}`} in ${formatInstallTargetRoot(record.targetRoot)} no longer exists in the registry.` };
|
|
664
|
+
}
|
|
665
|
+
return { record, reason: 'error', message: `Could not check ${record.skillName || `skill ${skillId}`}: ${error instanceof Error ? error.message : String(error)}` };
|
|
593
666
|
}
|
|
594
|
-
const detail = await client.get(`/api/skills/${record.skillId}`);
|
|
595
667
|
const latestVersion = normalizeText(detail.latestVersion);
|
|
596
668
|
if (!latestVersion) {
|
|
597
|
-
|
|
598
|
-
continue;
|
|
669
|
+
return { record, reason: 'no-version', message: `Skipping ${record.skillName}: no latest version is published.` };
|
|
599
670
|
}
|
|
600
671
|
if (latestVersion === normalizeText(record.skillVersion)) {
|
|
601
|
-
|
|
602
|
-
|
|
672
|
+
return { record, reason: 'current', message: `Skipping ${record.skillName}: ${latestVersion} is already installed in ${formatInstallTargetRoot(record.targetRoot)}.` };
|
|
673
|
+
}
|
|
674
|
+
return { record, detail, latestVersion };
|
|
675
|
+
});
|
|
676
|
+
const candidates = [];
|
|
677
|
+
for (const result of results) {
|
|
678
|
+
if ('reason' in result) {
|
|
679
|
+
options?.onSkipped?.(result);
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
candidates.push(result);
|
|
603
683
|
}
|
|
604
|
-
candidates.push({
|
|
605
|
-
record,
|
|
606
|
-
detail,
|
|
607
|
-
latestVersion,
|
|
608
|
-
});
|
|
609
684
|
}
|
|
610
685
|
return candidates;
|
|
611
686
|
}
|
|
687
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
688
|
+
const results = new Array(items.length);
|
|
689
|
+
let nextIndex = 0;
|
|
690
|
+
async function run() {
|
|
691
|
+
while (nextIndex < items.length) {
|
|
692
|
+
const index = nextIndex;
|
|
693
|
+
nextIndex += 1;
|
|
694
|
+
results[index] = await worker(items[index]);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, run));
|
|
698
|
+
return results;
|
|
699
|
+
}
|
|
612
700
|
function formatUpdateSelectionDescription(options) {
|
|
613
701
|
const parts = [];
|
|
614
702
|
if (options.skillRef) {
|
|
@@ -729,7 +817,7 @@ async function promptForAgents(scope) {
|
|
|
729
817
|
})),
|
|
730
818
|
],
|
|
731
819
|
initialValues,
|
|
732
|
-
required:
|
|
820
|
+
required: true,
|
|
733
821
|
});
|
|
734
822
|
if (isCancel(selected)) {
|
|
735
823
|
cancel('Cancelled.');
|
|
@@ -865,31 +953,81 @@ async function publishSkillFromDirectory(client, localSkill, input) {
|
|
|
865
953
|
}
|
|
866
954
|
async function writeSkillVersionToDirectory(client, input, installProgress) {
|
|
867
955
|
installProgress?.onTargetStart(input.targetRoot);
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
956
|
+
const previous = await findInstalledSkillRecord(client.baseUrl, input.targetRoot);
|
|
957
|
+
const expectedSizes = new Map(input.files.map((file) => [file.nameWithPath, file.length]));
|
|
958
|
+
const manifest = await installFilesAtomically({
|
|
959
|
+
targetRoot: input.targetRoot,
|
|
960
|
+
files: input.files.map((file) => ({ relativePath: file.nameWithPath })),
|
|
961
|
+
previousManifest: previous?.files,
|
|
962
|
+
async download(relativePath, destination) {
|
|
963
|
+
installProgress?.onFileStart(input.targetRoot, relativePath, expectedSizes.get(relativePath) ?? 0);
|
|
964
|
+
const response = await client.rawGet(`/api/skills/${input.skillId}/versions/${input.version}/files/${relativePath}`);
|
|
965
|
+
const hash = await downloadResponseToFile(response, destination, (bytes) => {
|
|
966
|
+
installProgress?.onBytesTransferred(bytes);
|
|
967
|
+
});
|
|
968
|
+
installProgress?.onFileComplete();
|
|
969
|
+
return hash;
|
|
970
|
+
},
|
|
971
|
+
});
|
|
878
972
|
await upsertInstalledSkillRecord(client.baseUrl, input.targetRoot, {
|
|
879
973
|
skillId: input.skillId,
|
|
880
974
|
skillName: input.skillName,
|
|
881
975
|
skillDescription: input.skillDescription,
|
|
882
976
|
skillVersion: input.version,
|
|
883
|
-
});
|
|
977
|
+
}, manifest);
|
|
884
978
|
installProgress?.onTargetComplete();
|
|
885
979
|
}
|
|
980
|
+
async function findInstalledSkillRecord(baseUrl, targetRoot) {
|
|
981
|
+
const resolvedTargetRoot = resolve(targetRoot);
|
|
982
|
+
const records = (await readInstalledSkillRecords()).filter((record) => record.targetRoot === resolvedTargetRoot);
|
|
983
|
+
return records.find((record) => record.baseUrl === normalizeBaseUrl(baseUrl)) ?? records[0] ?? null;
|
|
984
|
+
}
|
|
985
|
+
async function ensureTargetsWritable(baseUrl, targetRoots, options) {
|
|
986
|
+
if (options.force) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const conflicts = [];
|
|
990
|
+
for (const targetRoot of targetRoots) {
|
|
991
|
+
// Directories installed before installed.json existed still carry the legacy metadata file.
|
|
992
|
+
const record = await findInstalledSkillRecord(baseUrl, targetRoot)
|
|
993
|
+
?? (existsSync(join(targetRoot, SKILL_METADATA_FILE_NAME)) ? {} : null);
|
|
994
|
+
const state = await inspectInstallTarget(targetRoot, record);
|
|
995
|
+
if (state.state === 'modified') {
|
|
996
|
+
conflicts.push(` ${formatInstallTargetRoot(targetRoot)}: local changes in ${formatChangedPaths(state.paths)}`);
|
|
997
|
+
}
|
|
998
|
+
else if (state.state === 'untracked') {
|
|
999
|
+
conflicts.push(` ${formatInstallTargetRoot(targetRoot)}: contains files not installed by Skillshare`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
if (conflicts.length === 0) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (options.interactive) {
|
|
1006
|
+
log.warn(`These install locations would be overwritten:\n${conflicts.join('\n')}`);
|
|
1007
|
+
if (await promptConfirm('Overwrite them?', false)) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
cancel('Install cancelled. Nothing was changed.');
|
|
1011
|
+
process.exit(0);
|
|
1012
|
+
}
|
|
1013
|
+
throw new Error(`Refusing to overwrite:\n${conflicts.join('\n')}\nRe-run with --force to overwrite.`);
|
|
1014
|
+
}
|
|
1015
|
+
function formatChangedPaths(paths) {
|
|
1016
|
+
const shown = paths.slice(0, 3).join(', ');
|
|
1017
|
+
return paths.length > 3 ? `${shown} and ${paths.length - 3} more` : shown;
|
|
1018
|
+
}
|
|
1019
|
+
function formatCliPath(path) {
|
|
1020
|
+
const display = formatInstallTargetRoot(path);
|
|
1021
|
+
return /^[A-Za-z0-9._/~-]+$/.test(display) ? display : JSON.stringify(display);
|
|
1022
|
+
}
|
|
886
1023
|
async function downloadResponseToFile(response, destination, onBytesTransferred) {
|
|
1024
|
+
const hash = createHash('sha256');
|
|
887
1025
|
const body = response.body;
|
|
888
1026
|
if (!body) {
|
|
889
1027
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
890
1028
|
await writeFile(destination, buffer);
|
|
891
1029
|
onBytesTransferred?.(buffer.length);
|
|
892
|
-
return;
|
|
1030
|
+
return hash.update(buffer).digest('hex');
|
|
893
1031
|
}
|
|
894
1032
|
const fileHandle = await open(destination, 'w');
|
|
895
1033
|
let failed = true;
|
|
@@ -905,6 +1043,7 @@ async function downloadResponseToFile(response, destination, onBytesTransferred)
|
|
|
905
1043
|
continue;
|
|
906
1044
|
}
|
|
907
1045
|
await fileHandle.writeFile(value);
|
|
1046
|
+
hash.update(value);
|
|
908
1047
|
onBytesTransferred?.(value.length);
|
|
909
1048
|
}
|
|
910
1049
|
}
|
|
@@ -914,6 +1053,7 @@ async function downloadResponseToFile(response, destination, onBytesTransferred)
|
|
|
914
1053
|
await rm(destination, { force: true });
|
|
915
1054
|
}
|
|
916
1055
|
}
|
|
1056
|
+
return hash.digest('hex');
|
|
917
1057
|
}
|
|
918
1058
|
function createInstallProgressReporter(input) {
|
|
919
1059
|
const installStatus = spinner({ indicator: 'timer' });
|
|
@@ -1329,9 +1469,15 @@ async function login(baseUrl) {
|
|
|
1329
1469
|
const loginUrl = `${baseUrl}/api/cli/login?redirectUri=${encodeURIComponent(callback.redirectUri)}` +
|
|
1330
1470
|
`&codeChallenge=${encodeURIComponent(codeChallenge)}` +
|
|
1331
1471
|
`&state=${encodeURIComponent(state)}`;
|
|
1332
|
-
|
|
1333
|
-
console.log(`
|
|
1334
|
-
|
|
1472
|
+
console.log(`Opening the browser to sign in to ${baseUrl}.`);
|
|
1473
|
+
console.log(`If it does not open, visit this URL on this computer:\n ${loginUrl}`);
|
|
1474
|
+
try {
|
|
1475
|
+
await openBrowser(loginUrl);
|
|
1476
|
+
}
|
|
1477
|
+
catch {
|
|
1478
|
+
console.log('Could not open a browser automatically. Use the URL above.');
|
|
1479
|
+
}
|
|
1480
|
+
const authCode = await callback.wait(state, LOGIN_TIMEOUT_MS);
|
|
1335
1481
|
const response = await fetchJson(`${baseUrl}/api/cli/token`, {
|
|
1336
1482
|
method: 'POST',
|
|
1337
1483
|
headers: {
|
|
@@ -1353,6 +1499,7 @@ async function login(baseUrl) {
|
|
|
1353
1499
|
user: response.user,
|
|
1354
1500
|
});
|
|
1355
1501
|
console.log(`Logged in as ${response.user.displayName} (${baseUrl})`);
|
|
1502
|
+
return response.user;
|
|
1356
1503
|
}
|
|
1357
1504
|
const GCM_MINIMUM_VERSION = [2, 0, 931];
|
|
1358
1505
|
async function configureGitCredentialManager(baseUrl) {
|
|
@@ -1414,32 +1561,8 @@ function compareVersions(left, right) {
|
|
|
1414
1561
|
function formatVersion(version) {
|
|
1415
1562
|
return version.join('.');
|
|
1416
1563
|
}
|
|
1417
|
-
async function parseOptions(args) {
|
|
1418
|
-
const
|
|
1419
|
-
const flags = {};
|
|
1420
|
-
const positionals = [];
|
|
1421
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1422
|
-
const current = args[index];
|
|
1423
|
-
if (!current) {
|
|
1424
|
-
continue;
|
|
1425
|
-
}
|
|
1426
|
-
if (current.startsWith('--')) {
|
|
1427
|
-
const key = current.slice(2);
|
|
1428
|
-
const value = args[index + 1];
|
|
1429
|
-
if (!value || value.startsWith('--')) {
|
|
1430
|
-
if (booleanFlags.has(key)) {
|
|
1431
|
-
flags[key] = 'true';
|
|
1432
|
-
continue;
|
|
1433
|
-
}
|
|
1434
|
-
throw new Error(`Missing value for ${current}`);
|
|
1435
|
-
}
|
|
1436
|
-
flags[key] = value;
|
|
1437
|
-
index += 1;
|
|
1438
|
-
}
|
|
1439
|
-
else {
|
|
1440
|
-
positionals.push(current);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1564
|
+
async function parseOptions(args, commandName) {
|
|
1565
|
+
const { flags, positionals } = parseCliArgs(args, commandName);
|
|
1443
1566
|
const tokenFromFlag = normalizeText(flags.token);
|
|
1444
1567
|
const tokenFromEnvironment = normalizeText(process.env.SKILLSHARE_TOKEN);
|
|
1445
1568
|
return {
|
|
@@ -1476,7 +1599,7 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
1476
1599
|
if (new Date(currentSession.refreshTokenExpiresAt).getTime() <= Date.now()) {
|
|
1477
1600
|
throw new Error(`Stored session for ${resolvedBaseUrl} is no longer valid. Run "skillshare login${resolvedBaseUrl === LOCAL_URL ? ' local' : ''}" again.`);
|
|
1478
1601
|
}
|
|
1479
|
-
const response = await
|
|
1602
|
+
const response = await fetchFromServer(`${resolvedBaseUrl}/api/cli/refresh`, {
|
|
1480
1603
|
method: 'POST',
|
|
1481
1604
|
headers: {
|
|
1482
1605
|
'Content-Type': 'application/json',
|
|
@@ -1513,7 +1636,7 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
1513
1636
|
return refreshedSession.accessToken;
|
|
1514
1637
|
}
|
|
1515
1638
|
async function sendAuthorized(path, init, retryUnauthorized = true) {
|
|
1516
|
-
const response = await
|
|
1639
|
+
const response = await fetchFromServer(`${resolvedBaseUrl}${path}`, {
|
|
1517
1640
|
...init,
|
|
1518
1641
|
headers: await buildHeaders(() => getAccessToken(), init?.headers),
|
|
1519
1642
|
});
|
|
@@ -1521,7 +1644,7 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
1521
1644
|
return response;
|
|
1522
1645
|
}
|
|
1523
1646
|
const refreshedToken = await getAccessToken({ forceRefresh: true });
|
|
1524
|
-
return
|
|
1647
|
+
return fetchFromServer(`${resolvedBaseUrl}${path}`, {
|
|
1525
1648
|
...init,
|
|
1526
1649
|
headers: await buildHeaders(() => Promise.resolve(refreshedToken), init?.headers),
|
|
1527
1650
|
});
|
|
@@ -1538,10 +1661,14 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
1538
1661
|
});
|
|
1539
1662
|
return parseJson(response);
|
|
1540
1663
|
},
|
|
1664
|
+
async request(path, init) {
|
|
1665
|
+
// Streaming uploads cannot be replayed on an expired Skillshare access token.
|
|
1666
|
+
return sendAuthorized(path, { ...init, redirect: 'error' }, typeof init.body === 'string' || !init.body);
|
|
1667
|
+
},
|
|
1541
1668
|
async rawGet(path) {
|
|
1542
1669
|
const response = await sendAuthorized(path);
|
|
1543
1670
|
if (!response.ok) {
|
|
1544
|
-
throw new
|
|
1671
|
+
throw new HttpError(response.status, await formatErrorResponse(response));
|
|
1545
1672
|
}
|
|
1546
1673
|
return response;
|
|
1547
1674
|
},
|
|
@@ -1596,9 +1723,34 @@ async function buildHeaders(getAccessToken, initialHeaders) {
|
|
|
1596
1723
|
headers.set('Authorization', `Bearer ${await getAccessToken()}`);
|
|
1597
1724
|
return headers;
|
|
1598
1725
|
}
|
|
1726
|
+
class HttpError extends Error {
|
|
1727
|
+
status;
|
|
1728
|
+
constructor(status, message) {
|
|
1729
|
+
super(message);
|
|
1730
|
+
this.status = status;
|
|
1731
|
+
this.name = 'HttpError';
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
class NetworkError extends Error {
|
|
1735
|
+
constructor(origin, cause) {
|
|
1736
|
+
super(`Cannot reach ${origin}. Check your network or VPN connection and try again.`, { cause });
|
|
1737
|
+
this.name = 'NetworkError';
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
async function fetchFromServer(input, init) {
|
|
1741
|
+
try {
|
|
1742
|
+
return await fetch(input, init);
|
|
1743
|
+
}
|
|
1744
|
+
catch (error) {
|
|
1745
|
+
if (init?.signal?.aborted) {
|
|
1746
|
+
throw error;
|
|
1747
|
+
}
|
|
1748
|
+
throw new NetworkError(new URL(input).origin, error);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1599
1751
|
async function parseJson(response) {
|
|
1600
1752
|
if (!response.ok) {
|
|
1601
|
-
throw new
|
|
1753
|
+
throw new HttpError(response.status, await formatErrorResponse(response));
|
|
1602
1754
|
}
|
|
1603
1755
|
return response.json();
|
|
1604
1756
|
}
|
|
@@ -1633,7 +1785,7 @@ async function formatErrorResponse(response) {
|
|
|
1633
1785
|
: `${fallbackMessage} ${body}`;
|
|
1634
1786
|
}
|
|
1635
1787
|
async function fetchJson(input, init) {
|
|
1636
|
-
const response = await
|
|
1788
|
+
const response = await fetchFromServer(input, init);
|
|
1637
1789
|
return parseJson(response);
|
|
1638
1790
|
}
|
|
1639
1791
|
async function loadSession(baseUrl) {
|
|
@@ -1739,15 +1891,22 @@ async function waitForLoginCallback() {
|
|
|
1739
1891
|
}
|
|
1740
1892
|
return {
|
|
1741
1893
|
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
1742
|
-
async wait(expectedState) {
|
|
1894
|
+
async wait(expectedState, timeoutMs) {
|
|
1895
|
+
let timer;
|
|
1896
|
+
const timeout = new Promise((_, reject) => {
|
|
1897
|
+
timer = setTimeout(() => {
|
|
1898
|
+
reject(new Error(`Sign-in did not complete within ${Math.round(timeoutMs / 60_000)} minutes. Run "skillshare login" again.`));
|
|
1899
|
+
}, timeoutMs);
|
|
1900
|
+
});
|
|
1743
1901
|
try {
|
|
1744
|
-
const payload = JSON.parse(await statePromise);
|
|
1902
|
+
const payload = JSON.parse(await Promise.race([statePromise, timeout]));
|
|
1745
1903
|
if (payload.state !== expectedState) {
|
|
1746
1904
|
throw new Error('Login state mismatch.');
|
|
1747
1905
|
}
|
|
1748
1906
|
return payload.code;
|
|
1749
1907
|
}
|
|
1750
1908
|
finally {
|
|
1909
|
+
clearTimeout(timer);
|
|
1751
1910
|
server.close();
|
|
1752
1911
|
}
|
|
1753
1912
|
},
|
|
@@ -1804,7 +1963,7 @@ async function upsertUploadedSkillMetadata(baseUrl, directory, metadata) {
|
|
|
1804
1963
|
await writeStateFile(UPLOADED_STATE_PATH, records);
|
|
1805
1964
|
await removeLegacyLocalSkillMetadata(resolvedDirectory);
|
|
1806
1965
|
}
|
|
1807
|
-
async function upsertInstalledSkillRecord(baseUrl, targetRoot, metadata) {
|
|
1966
|
+
async function upsertInstalledSkillRecord(baseUrl, targetRoot, metadata, files) {
|
|
1808
1967
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
1809
1968
|
const resolvedTargetRoot = resolve(targetRoot);
|
|
1810
1969
|
const records = (await readInstalledSkillRecords())
|
|
@@ -1814,6 +1973,7 @@ async function upsertInstalledSkillRecord(baseUrl, targetRoot, metadata) {
|
|
|
1814
1973
|
targetRoot: resolvedTargetRoot,
|
|
1815
1974
|
baseUrl: normalizedBaseUrl,
|
|
1816
1975
|
updatedAt: new Date().toISOString(),
|
|
1976
|
+
...(files ? { files } : {}),
|
|
1817
1977
|
});
|
|
1818
1978
|
await writeStateFile(INSTALLED_STATE_PATH, records);
|
|
1819
1979
|
await removeLegacyLocalSkillMetadata(resolvedTargetRoot);
|
|
@@ -1855,9 +2015,17 @@ async function readInstalledSkillRecords() {
|
|
|
1855
2015
|
skillName: typeof entry.skillName === 'string' ? entry.skillName : '',
|
|
1856
2016
|
skillDescription: typeof entry.skillDescription === 'string' ? entry.skillDescription : '',
|
|
1857
2017
|
skillVersion: typeof entry.skillVersion === 'string' ? entry.skillVersion : '',
|
|
2018
|
+
files: readFileManifest(entry.files),
|
|
1858
2019
|
}].filter((record) => record.targetRoot.length > 0);
|
|
1859
2020
|
});
|
|
1860
2021
|
}
|
|
2022
|
+
function readFileManifest(value) {
|
|
2023
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2024
|
+
return undefined;
|
|
2025
|
+
}
|
|
2026
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === 'string');
|
|
2027
|
+
return Object.fromEntries(entries);
|
|
2028
|
+
}
|
|
1861
2029
|
async function readStateFile(path) {
|
|
1862
2030
|
try {
|
|
1863
2031
|
const parsed = JSON.parse(await readFile(path, 'utf8'));
|