@quadient/skillshare 1.2.0 → 1.3.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 +14 -0
  2. package/dist/index.js +121 -8
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -156,6 +156,20 @@ npx @quadient/skillshare update "Prompt Lint" --dry-run
156
156
  npx @quadient/skillshare update 12 --target ./installed
157
157
  ```
158
158
 
159
+ Publish an authenticated static page. The upload replaces the page's complete
160
+ content, and the directory must contain `index.html` at its root. By default the
161
+ directory name is used as the page slug:
162
+
163
+ ```bash
164
+ npx @quadient/skillshare pages upload ./release-notes
165
+ npx @quadient/skillshare pages upload ./site --slug release-notes
166
+ npx @quadient/skillshare pages list
167
+ npx @quadient/skillshare pages show release-notes
168
+ ```
169
+
170
+ The page is then available at
171
+ `https://skillshare.inspireclouduat.net/p/release-notes/` after SSO sign-in.
172
+
159
173
  ## Package Distribution
160
174
 
161
175
  The CLI is prepared for publishing as `@quadient/skillshare`.
package/dist/index.js CHANGED
@@ -78,6 +78,9 @@ 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>]
81
84
 
82
85
  Options:
83
86
  --token <token> Bearer token for API requests (overrides SKILLSHARE_TOKEN)
@@ -108,7 +111,9 @@ async function main() {
108
111
  case 'show':
109
112
  case 'upload':
110
113
  case 'install':
111
- case 'update': {
114
+ case 'update':
115
+ case 'page':
116
+ case 'pages': {
112
117
  const activeBaseUrl = await resolveActiveBaseUrl();
113
118
  const client = await createClient(activeBaseUrl, options.token);
114
119
  switch (command) {
@@ -254,6 +259,45 @@ async function main() {
254
259
  });
255
260
  return;
256
261
  }
262
+ case 'page':
263
+ case 'pages': {
264
+ const action = options.positionals[0] ?? 'list';
265
+ if (action === 'list') {
266
+ const pages = await client.get('/api/pages');
267
+ for (const page of pages) {
268
+ console.log(`${page.slug}\t${page.fileCount}\t${page.ownerDisplayName}`);
269
+ }
270
+ return;
271
+ }
272
+ if (action === 'show') {
273
+ const slug = options.positionals[1];
274
+ if (!slug)
275
+ throw new Error('pages show requires <slug>');
276
+ console.log(JSON.stringify(await client.get(`/api/pages/${encodeURIComponent(slug)}`), null, 2));
277
+ return;
278
+ }
279
+ if (action === 'upload') {
280
+ const directory = options.positionals[1];
281
+ if (!directory)
282
+ throw new Error('pages upload requires <directory>');
283
+ const resolvedDirectory = resolve(directory);
284
+ const slug = normalizePageSlug(options.flags.slug ?? basename(resolvedDirectory));
285
+ const pageFiles = await loadLocalPageFiles(resolvedDirectory);
286
+ const uploadStatus = spinner({ indicator: 'timer' });
287
+ uploadStatus.start(`Uploading ${pageFiles.files.length} files (${formatByteSize(pageFiles.totalBytes)}) to /p/${slug}/`);
288
+ try {
289
+ const detail = await client.replacePageContent(slug, pageFiles.files);
290
+ uploadStatus.stop(`Published /p/${detail.slug}/ with ${detail.files.length} files`);
291
+ console.log(JSON.stringify(detail, null, 2));
292
+ }
293
+ catch (error) {
294
+ uploadStatus.clear();
295
+ throw error;
296
+ }
297
+ return;
298
+ }
299
+ throw new Error(`Unknown pages command '${action}'. Use list, show, or upload.`);
300
+ }
257
301
  }
258
302
  return;
259
303
  }
@@ -736,6 +780,20 @@ async function loadLocalSkillContext(directory, baseUrl) {
736
780
  defaults: deriveSkillDefaults(files, resolvedDirectory),
737
781
  };
738
782
  }
783
+ async function loadLocalPageFiles(directory) {
784
+ const filePlans = await collectFilePlans(directory, directory, { includeSkillMetadata: true });
785
+ if (filePlans.length === 0) {
786
+ throw new Error('The page directory is empty.');
787
+ }
788
+ if (!filePlans.some((file) => file.relativePath === 'index.html')) {
789
+ throw new Error('The page directory must contain index.html at its root.');
790
+ }
791
+ const totalBytes = filePlans.reduce((sum, file) => sum + file.size, 0);
792
+ return {
793
+ files: await collectFiles(filePlans),
794
+ totalBytes,
795
+ };
796
+ }
739
797
  async function createSkillFromDirectory(client, localSkill, input) {
740
798
  const name = normalizeText(input.name) ?? localSkill.defaults.name;
741
799
  const description = normalizeText(input.description) ?? localSkill.defaults.description;
@@ -1095,6 +1153,13 @@ function normalizeText(value) {
1095
1153
  const trimmed = value?.trim();
1096
1154
  return trimmed ? trimmed : null;
1097
1155
  }
1156
+ function normalizePageSlug(value) {
1157
+ const normalized = value.trim().toLowerCase();
1158
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(normalized)) {
1159
+ throw new Error('Page slug must be 1-64 lowercase letters, numbers, or hyphens, and cannot start or end with a hyphen.');
1160
+ }
1161
+ return normalized;
1162
+ }
1098
1163
  async function resolveExistingUploadSkill(client, localSkill, options) {
1099
1164
  const explicitSkillId = normalizeText(options?.explicitSkillId);
1100
1165
  if (explicitSkillId) {
@@ -1424,6 +1489,17 @@ async function createClient(baseUrl, explicitToken) {
1424
1489
  });
1425
1490
  return parseJson(response);
1426
1491
  },
1492
+ async replacePageContent(slug, files) {
1493
+ const formData = new FormData();
1494
+ for (const file of files) {
1495
+ formData.append('files', new Blob([toBlobPart(file.content)], { type: file.contentType }), file.relativePath);
1496
+ }
1497
+ const response = await sendAuthorized(`/api/pages/${encodeURIComponent(slug)}/content`, {
1498
+ method: 'PUT',
1499
+ body: formData,
1500
+ });
1501
+ return parseJson(response);
1502
+ },
1427
1503
  };
1428
1504
  }
1429
1505
  async function buildHeaders(getAccessToken, initialHeaders) {
@@ -1725,13 +1801,13 @@ async function removeLegacyLocalSkillMetadata(directory) {
1725
1801
  throw error;
1726
1802
  }
1727
1803
  }
1728
- async function collectFilePlans(root, current = root) {
1804
+ async function collectFilePlans(root, current = root, options = {}) {
1729
1805
  const entries = await readdir(current, { withFileTypes: true });
1730
1806
  const files = [];
1731
1807
  for (const entry of entries) {
1732
1808
  const entryPath = join(current, entry.name);
1733
1809
  if (entry.isDirectory()) {
1734
- files.push(...(await collectFilePlans(root, entryPath)));
1810
+ files.push(...(await collectFilePlans(root, entryPath, options)));
1735
1811
  continue;
1736
1812
  }
1737
1813
  const entryStat = await stat(entryPath);
@@ -1739,7 +1815,7 @@ async function collectFilePlans(root, current = root) {
1739
1815
  continue;
1740
1816
  }
1741
1817
  const relativePath = relative(root, entryPath).replaceAll('\\', '/');
1742
- if (relativePath === SKILL_METADATA_FILE_NAME) {
1818
+ if (!options.includeSkillMetadata && relativePath === SKILL_METADATA_FILE_NAME) {
1743
1819
  continue;
1744
1820
  }
1745
1821
  files.push({
@@ -1775,14 +1851,51 @@ function validateSkillDirectory(files) {
1775
1851
  }
1776
1852
  }
1777
1853
  function guessContentType(path) {
1778
- if (path.endsWith('.md'))
1854
+ const normalized = path.toLowerCase();
1855
+ if (normalized.endsWith('.html') || normalized.endsWith('.htm'))
1856
+ return 'text/html; charset=utf-8';
1857
+ if (normalized.endsWith('.css'))
1858
+ return 'text/css; charset=utf-8';
1859
+ if (normalized.endsWith('.js') || normalized.endsWith('.mjs'))
1860
+ return 'text/javascript; charset=utf-8';
1861
+ if (normalized.endsWith('.md'))
1779
1862
  return 'text/markdown; charset=utf-8';
1780
- if (path.endsWith('.json'))
1863
+ if (normalized.endsWith('.json') || normalized.endsWith('.webmanifest'))
1781
1864
  return 'application/json; charset=utf-8';
1782
- if (path.endsWith('.yaml') || path.endsWith('.yml'))
1865
+ if (normalized.endsWith('.yaml') || normalized.endsWith('.yml'))
1783
1866
  return 'application/yaml; charset=utf-8';
1784
- if (path.endsWith('.txt'))
1867
+ if (normalized.endsWith('.txt') || normalized.endsWith('.csv'))
1785
1868
  return 'text/plain; charset=utf-8';
1869
+ if (normalized.endsWith('.svg'))
1870
+ return 'image/svg+xml';
1871
+ if (normalized.endsWith('.png'))
1872
+ return 'image/png';
1873
+ if (normalized.endsWith('.jpg') || normalized.endsWith('.jpeg'))
1874
+ return 'image/jpeg';
1875
+ if (normalized.endsWith('.webp'))
1876
+ return 'image/webp';
1877
+ if (normalized.endsWith('.gif'))
1878
+ return 'image/gif';
1879
+ if (normalized.endsWith('.avif'))
1880
+ return 'image/avif';
1881
+ if (normalized.endsWith('.ico'))
1882
+ return 'image/x-icon';
1883
+ if (normalized.endsWith('.mp4'))
1884
+ return 'video/mp4';
1885
+ if (normalized.endsWith('.webm'))
1886
+ return 'video/webm';
1887
+ if (normalized.endsWith('.mp3'))
1888
+ return 'audio/mpeg';
1889
+ if (normalized.endsWith('.wav'))
1890
+ return 'audio/wav';
1891
+ if (normalized.endsWith('.pdf'))
1892
+ return 'application/pdf';
1893
+ if (normalized.endsWith('.wasm'))
1894
+ return 'application/wasm';
1895
+ if (normalized.endsWith('.woff'))
1896
+ return 'font/woff';
1897
+ if (normalized.endsWith('.woff2'))
1898
+ return 'font/woff2';
1786
1899
  return 'application/octet-stream';
1787
1900
  }
1788
1901
  function formatByteSize(bytes) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quadient/skillshare",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Skillshare CLI for installing and publishing Quadient skills",
5
5
  "type": "module",
6
6
  "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
  }