@quadient/skillshare 1.2.0 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +25 -0
  2. package/dist/index.js +196 -8
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -148,6 +148,17 @@ export SKILLSHARE_TOKEN="<personal-access-token>"
148
148
  npx @quadient/skillshare whoami
149
149
  ```
150
150
 
151
+ Register the production or local SkillShare host as a generic OAuth provider in Git Credential
152
+ Manager 2.0.931 or newer:
153
+
154
+ ```bash
155
+ skillshare git configure
156
+ skillshare git configure local
157
+ ```
158
+
159
+ Remove only the URL-scoped SkillShare registration with `skillshare git unconfigure [local]`.
160
+ The first Git clone opens the browser login; no SkillShare personal access token needs to be created.
161
+
151
162
  Update recorded installs to the latest versions:
152
163
 
153
164
  ```bash
@@ -156,6 +167,20 @@ npx @quadient/skillshare update "Prompt Lint" --dry-run
156
167
  npx @quadient/skillshare update 12 --target ./installed
157
168
  ```
158
169
 
170
+ Publish an authenticated static page. The upload replaces the page's complete
171
+ content, and the directory must contain `index.html` at its root. By default the
172
+ directory name is used as the page slug:
173
+
174
+ ```bash
175
+ npx @quadient/skillshare pages upload ./release-notes
176
+ npx @quadient/skillshare pages upload ./site --slug release-notes
177
+ npx @quadient/skillshare pages list
178
+ npx @quadient/skillshare pages show release-notes
179
+ ```
180
+
181
+ The page is then available at
182
+ `https://skillshare.inspireclouduat.net/p/release-notes/` after SSO sign-in.
183
+
159
184
  ## Package Distribution
160
185
 
161
186
  The CLI is prepared for publishing as `@quadient/skillshare`.
package/dist/index.js CHANGED
@@ -78,6 +78,11 @@ Commands:
78
78
  install <skillId|skillName> [version] --target <directory>
79
79
  install --skill-version-id <id> --target <directory>
80
80
  update [skillId|skillName] [--target <directory>] [--dry-run]
81
+ pages list
82
+ pages show <slug>
83
+ pages upload <directory> [--slug <slug>]
84
+ git configure [local]
85
+ git unconfigure [local]
81
86
 
82
87
  Options:
83
88
  --token <token> Bearer token for API requests (overrides SKILLSHARE_TOKEN)
@@ -103,12 +108,27 @@ async function main() {
103
108
  await logoutSession();
104
109
  return;
105
110
  }
111
+ case 'git': {
112
+ const action = options.positionals[0];
113
+ const baseUrl = options.positionals[1] === 'local' ? LOCAL_URL : PROD_URL;
114
+ if (action === 'configure') {
115
+ await configureGitCredentialManager(baseUrl);
116
+ return;
117
+ }
118
+ if (action === 'unconfigure') {
119
+ await unconfigureGitCredentialManager(baseUrl);
120
+ return;
121
+ }
122
+ throw new Error('git requires "configure [local]" or "unconfigure [local]".');
123
+ }
106
124
  case 'whoami':
107
125
  case 'list':
108
126
  case 'show':
109
127
  case 'upload':
110
128
  case 'install':
111
- case 'update': {
129
+ case 'update':
130
+ case 'page':
131
+ case 'pages': {
112
132
  const activeBaseUrl = await resolveActiveBaseUrl();
113
133
  const client = await createClient(activeBaseUrl, options.token);
114
134
  switch (command) {
@@ -254,6 +274,45 @@ async function main() {
254
274
  });
255
275
  return;
256
276
  }
277
+ case 'page':
278
+ case 'pages': {
279
+ const action = options.positionals[0] ?? 'list';
280
+ if (action === 'list') {
281
+ const pages = await client.get('/api/pages');
282
+ for (const page of pages) {
283
+ console.log(`${page.slug}\t${page.fileCount}\t${page.ownerDisplayName}`);
284
+ }
285
+ return;
286
+ }
287
+ if (action === 'show') {
288
+ const slug = options.positionals[1];
289
+ if (!slug)
290
+ throw new Error('pages show requires <slug>');
291
+ console.log(JSON.stringify(await client.get(`/api/pages/${encodeURIComponent(slug)}`), null, 2));
292
+ return;
293
+ }
294
+ if (action === 'upload') {
295
+ const directory = options.positionals[1];
296
+ if (!directory)
297
+ throw new Error('pages upload requires <directory>');
298
+ const resolvedDirectory = resolve(directory);
299
+ const slug = normalizePageSlug(options.flags.slug ?? basename(resolvedDirectory));
300
+ const pageFiles = await loadLocalPageFiles(resolvedDirectory);
301
+ const uploadStatus = spinner({ indicator: 'timer' });
302
+ uploadStatus.start(`Uploading ${pageFiles.files.length} files (${formatByteSize(pageFiles.totalBytes)}) to /p/${slug}/`);
303
+ try {
304
+ const detail = await client.replacePageContent(slug, pageFiles.files);
305
+ uploadStatus.stop(`Published /p/${detail.slug}/ with ${detail.files.length} files`);
306
+ console.log(JSON.stringify(detail, null, 2));
307
+ }
308
+ catch (error) {
309
+ uploadStatus.clear();
310
+ throw error;
311
+ }
312
+ return;
313
+ }
314
+ throw new Error(`Unknown pages command '${action}'. Use list, show, or upload.`);
315
+ }
257
316
  }
258
317
  return;
259
318
  }
@@ -736,6 +795,20 @@ async function loadLocalSkillContext(directory, baseUrl) {
736
795
  defaults: deriveSkillDefaults(files, resolvedDirectory),
737
796
  };
738
797
  }
798
+ async function loadLocalPageFiles(directory) {
799
+ const filePlans = await collectFilePlans(directory, directory, { includeSkillMetadata: true });
800
+ if (filePlans.length === 0) {
801
+ throw new Error('The page directory is empty.');
802
+ }
803
+ if (!filePlans.some((file) => file.relativePath === 'index.html')) {
804
+ throw new Error('The page directory must contain index.html at its root.');
805
+ }
806
+ const totalBytes = filePlans.reduce((sum, file) => sum + file.size, 0);
807
+ return {
808
+ files: await collectFiles(filePlans),
809
+ totalBytes,
810
+ };
811
+ }
739
812
  async function createSkillFromDirectory(client, localSkill, input) {
740
813
  const name = normalizeText(input.name) ?? localSkill.defaults.name;
741
814
  const description = normalizeText(input.description) ?? localSkill.defaults.description;
@@ -1095,6 +1168,13 @@ function normalizeText(value) {
1095
1168
  const trimmed = value?.trim();
1096
1169
  return trimmed ? trimmed : null;
1097
1170
  }
1171
+ function normalizePageSlug(value) {
1172
+ const normalized = value.trim().toLowerCase();
1173
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(normalized)) {
1174
+ throw new Error('Page slug must be 1-64 lowercase letters, numbers, or hyphens, and cannot start or end with a hyphen.');
1175
+ }
1176
+ return normalized;
1177
+ }
1098
1178
  async function resolveExistingUploadSkill(client, localSkill, options) {
1099
1179
  const explicitSkillId = normalizeText(options?.explicitSkillId);
1100
1180
  if (explicitSkillId) {
@@ -1263,6 +1343,66 @@ async function login(baseUrl) {
1263
1343
  });
1264
1344
  console.log(`Logged in as ${response.user.displayName} (${baseUrl})`);
1265
1345
  }
1346
+ const GCM_MINIMUM_VERSION = [2, 0, 931];
1347
+ async function configureGitCredentialManager(baseUrl) {
1348
+ baseUrl = normalizeBaseUrl(baseUrl);
1349
+ const versionResult = await runProcess('git', ['credential-manager', '--version'], { allowFailure: true });
1350
+ const version = parseVersion(`${versionResult.stdout} ${versionResult.stderr}`);
1351
+ if (versionResult.exitCode !== 0 || !version || compareVersions(version, GCM_MINIMUM_VERSION) < 0) {
1352
+ throw new Error('Git Credential Manager 2.0.931 or newer is required. Install or update GCM, then run this command again.');
1353
+ }
1354
+ await runProcess('git', ['credential-manager', 'configure']);
1355
+ const prefix = `credential.${baseUrl}`;
1356
+ const settings = [
1357
+ [`${prefix}.provider`, 'generic'],
1358
+ [`${prefix}.oauthClientId`, 'skillshare-gcm'],
1359
+ [`${prefix}.oauthRedirectUri`, 'http://127.0.0.1'],
1360
+ [`${prefix}.oauthAuthorizeEndpoint`, '/api/git/oauth/authorize'],
1361
+ [`${prefix}.oauthTokenEndpoint`, '/api/git/oauth/token'],
1362
+ [`${prefix}.oauthScopes`, 'git.read'],
1363
+ [`${prefix}.oauthDefaultUserName`, 'skillshare'],
1364
+ [`${prefix}.oauthUseClientAuthHeader`, 'false'],
1365
+ [`${prefix}.useHttpPath`, 'false'],
1366
+ ];
1367
+ for (const [key, value] of settings) {
1368
+ await runProcess('git', ['config', '--global', '--replace-all', key, value]);
1369
+ }
1370
+ console.log(`Registered ${baseUrl} in Git Credential Manager ${formatVersion(version)}.`);
1371
+ }
1372
+ async function unconfigureGitCredentialManager(baseUrl) {
1373
+ baseUrl = normalizeBaseUrl(baseUrl);
1374
+ const prefix = `credential.${baseUrl}`;
1375
+ for (const key of [
1376
+ 'provider',
1377
+ 'oauthClientId',
1378
+ 'oauthRedirectUri',
1379
+ 'oauthAuthorizeEndpoint',
1380
+ 'oauthTokenEndpoint',
1381
+ 'oauthScopes',
1382
+ 'oauthDefaultUserName',
1383
+ 'oauthUseClientAuthHeader',
1384
+ 'useHttpPath',
1385
+ ]) {
1386
+ await runProcess('git', ['config', '--global', '--unset-all', `${prefix}.${key}`], { allowFailure: true });
1387
+ }
1388
+ console.log(`Removed the Git Credential Manager registration for ${baseUrl}.`);
1389
+ }
1390
+ function parseVersion(value) {
1391
+ const match = value.match(/(?:^|\s)(\d+)\.(\d+)\.(\d+)(?:\D|$)/);
1392
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
1393
+ }
1394
+ function compareVersions(left, right) {
1395
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
1396
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
1397
+ if (difference !== 0) {
1398
+ return difference;
1399
+ }
1400
+ }
1401
+ return 0;
1402
+ }
1403
+ function formatVersion(version) {
1404
+ return version.join('.');
1405
+ }
1266
1406
  async function parseOptions(args) {
1267
1407
  const booleanFlags = new Set(['dry-run']);
1268
1408
  const flags = {};
@@ -1424,6 +1564,17 @@ async function createClient(baseUrl, explicitToken) {
1424
1564
  });
1425
1565
  return parseJson(response);
1426
1566
  },
1567
+ async replacePageContent(slug, files) {
1568
+ const formData = new FormData();
1569
+ for (const file of files) {
1570
+ formData.append('files', new Blob([toBlobPart(file.content)], { type: file.contentType }), file.relativePath);
1571
+ }
1572
+ const response = await sendAuthorized(`/api/pages/${encodeURIComponent(slug)}/content`, {
1573
+ method: 'PUT',
1574
+ body: formData,
1575
+ });
1576
+ return parseJson(response);
1577
+ },
1427
1578
  };
1428
1579
  }
1429
1580
  async function buildHeaders(getAccessToken, initialHeaders) {
@@ -1725,13 +1876,13 @@ async function removeLegacyLocalSkillMetadata(directory) {
1725
1876
  throw error;
1726
1877
  }
1727
1878
  }
1728
- async function collectFilePlans(root, current = root) {
1879
+ async function collectFilePlans(root, current = root, options = {}) {
1729
1880
  const entries = await readdir(current, { withFileTypes: true });
1730
1881
  const files = [];
1731
1882
  for (const entry of entries) {
1732
1883
  const entryPath = join(current, entry.name);
1733
1884
  if (entry.isDirectory()) {
1734
- files.push(...(await collectFilePlans(root, entryPath)));
1885
+ files.push(...(await collectFilePlans(root, entryPath, options)));
1735
1886
  continue;
1736
1887
  }
1737
1888
  const entryStat = await stat(entryPath);
@@ -1739,7 +1890,7 @@ async function collectFilePlans(root, current = root) {
1739
1890
  continue;
1740
1891
  }
1741
1892
  const relativePath = relative(root, entryPath).replaceAll('\\', '/');
1742
- if (relativePath === SKILL_METADATA_FILE_NAME) {
1893
+ if (!options.includeSkillMetadata && relativePath === SKILL_METADATA_FILE_NAME) {
1743
1894
  continue;
1744
1895
  }
1745
1896
  files.push({
@@ -1775,14 +1926,51 @@ function validateSkillDirectory(files) {
1775
1926
  }
1776
1927
  }
1777
1928
  function guessContentType(path) {
1778
- if (path.endsWith('.md'))
1929
+ const normalized = path.toLowerCase();
1930
+ if (normalized.endsWith('.html') || normalized.endsWith('.htm'))
1931
+ return 'text/html; charset=utf-8';
1932
+ if (normalized.endsWith('.css'))
1933
+ return 'text/css; charset=utf-8';
1934
+ if (normalized.endsWith('.js') || normalized.endsWith('.mjs'))
1935
+ return 'text/javascript; charset=utf-8';
1936
+ if (normalized.endsWith('.md'))
1779
1937
  return 'text/markdown; charset=utf-8';
1780
- if (path.endsWith('.json'))
1938
+ if (normalized.endsWith('.json') || normalized.endsWith('.webmanifest'))
1781
1939
  return 'application/json; charset=utf-8';
1782
- if (path.endsWith('.yaml') || path.endsWith('.yml'))
1940
+ if (normalized.endsWith('.yaml') || normalized.endsWith('.yml'))
1783
1941
  return 'application/yaml; charset=utf-8';
1784
- if (path.endsWith('.txt'))
1942
+ if (normalized.endsWith('.txt') || normalized.endsWith('.csv'))
1785
1943
  return 'text/plain; charset=utf-8';
1944
+ if (normalized.endsWith('.svg'))
1945
+ return 'image/svg+xml';
1946
+ if (normalized.endsWith('.png'))
1947
+ return 'image/png';
1948
+ if (normalized.endsWith('.jpg') || normalized.endsWith('.jpeg'))
1949
+ return 'image/jpeg';
1950
+ if (normalized.endsWith('.webp'))
1951
+ return 'image/webp';
1952
+ if (normalized.endsWith('.gif'))
1953
+ return 'image/gif';
1954
+ if (normalized.endsWith('.avif'))
1955
+ return 'image/avif';
1956
+ if (normalized.endsWith('.ico'))
1957
+ return 'image/x-icon';
1958
+ if (normalized.endsWith('.mp4'))
1959
+ return 'video/mp4';
1960
+ if (normalized.endsWith('.webm'))
1961
+ return 'video/webm';
1962
+ if (normalized.endsWith('.mp3'))
1963
+ return 'audio/mpeg';
1964
+ if (normalized.endsWith('.wav'))
1965
+ return 'audio/wav';
1966
+ if (normalized.endsWith('.pdf'))
1967
+ return 'application/pdf';
1968
+ if (normalized.endsWith('.wasm'))
1969
+ return 'application/wasm';
1970
+ if (normalized.endsWith('.woff'))
1971
+ return 'font/woff';
1972
+ if (normalized.endsWith('.woff2'))
1973
+ return 'font/woff2';
1786
1974
  return 'application/octet-stream';
1787
1975
  }
1788
1976
  function formatByteSize(bytes) {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@quadient/skillshare",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Skillshare CLI for installing and publishing Quadient skills",
5
5
  "type": "module",
6
6
  "bin": {
7
- "skillshare": "./bin/skillshare"
7
+ "skillshare": "bin/skillshare"
8
8
  },
9
9
  "files": [
10
10
  "bin",
@@ -21,14 +21,14 @@
21
21
  "access": "public"
22
22
  },
23
23
  "devDependencies": {
24
- "@types/node": "^24.6.0",
25
- "tsx": "^4.20.5",
26
- "typescript": "^5.9.3"
24
+ "@types/node": "^26.1.1",
25
+ "tsx": "^4.23.1",
26
+ "typescript": "^7.0.2"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=20"
30
30
  },
31
31
  "dependencies": {
32
- "@clack/prompts": "^1.2.0"
32
+ "@clack/prompts": "^1.7.0"
33
33
  }
34
34
  }