@quadient/skillshare 1.3.0 → 1.5.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 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
@@ -198,3 +209,11 @@ bun run publish:tui -- --otp 123456
198
209
  ```
199
210
 
200
211
  The publish script builds `tui/`, validates the packed files with `npm pack --dry-run`, and only then runs `npm publish`.
212
+
213
+ ## Pages applications
214
+
215
+ `skillshare pages upload <directory> --slug <slug>` accepts static sites or
216
+ Wrangler JSON/JSONC projects. The CLI bundles Worker dependencies locally; install
217
+ them and build frontend assets before uploading. Inspect process status and logs
218
+ with `skillshare pages runtime <slug>`. Pages are served below `/p/<slug>/`, so use
219
+ that base path when building assets. Runtime data survives content replacement.
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { preparePageFiles } from './pages.js';
1
2
  import { createServer } from 'node:http';
2
3
  import { createHash, randomBytes } from 'node:crypto';
3
4
  import { spawn } from 'node:child_process';
@@ -81,6 +82,8 @@ Commands:
81
82
  pages list
82
83
  pages show <slug>
83
84
  pages upload <directory> [--slug <slug>]
85
+ git configure [local]
86
+ git unconfigure [local]
84
87
 
85
88
  Options:
86
89
  --token <token> Bearer token for API requests (overrides SKILLSHARE_TOKEN)
@@ -106,6 +109,19 @@ async function main() {
106
109
  await logoutSession();
107
110
  return;
108
111
  }
112
+ case 'git': {
113
+ const action = options.positionals[0];
114
+ const baseUrl = options.positionals[1] === 'local' ? LOCAL_URL : PROD_URL;
115
+ if (action === 'configure') {
116
+ await configureGitCredentialManager(baseUrl);
117
+ return;
118
+ }
119
+ if (action === 'unconfigure') {
120
+ await unconfigureGitCredentialManager(baseUrl);
121
+ return;
122
+ }
123
+ throw new Error('git requires "configure [local]" or "unconfigure [local]".');
124
+ }
109
125
  case 'whoami':
110
126
  case 'list':
111
127
  case 'show':
@@ -265,15 +281,15 @@ async function main() {
265
281
  if (action === 'list') {
266
282
  const pages = await client.get('/api/pages');
267
283
  for (const page of pages) {
268
- console.log(`${page.slug}\t${page.fileCount}\t${page.ownerDisplayName}`);
284
+ console.log(`${page.slug}\t${page.kind}\t${page.fileCount}\t${page.ownerDisplayName}`);
269
285
  }
270
286
  return;
271
287
  }
272
- if (action === 'show') {
288
+ if (action === 'show' || action === 'runtime') {
273
289
  const slug = options.positionals[1];
274
290
  if (!slug)
275
- throw new Error('pages show requires <slug>');
276
- console.log(JSON.stringify(await client.get(`/api/pages/${encodeURIComponent(slug)}`), null, 2));
291
+ throw new Error(`pages ${action} requires <slug>`);
292
+ console.log(JSON.stringify(await client.get(`/api/pages/${encodeURIComponent(slug)}${action === 'runtime' ? '/runtime' : ''}`), null, 2));
277
293
  return;
278
294
  }
279
295
  if (action === 'upload') {
@@ -296,7 +312,7 @@ async function main() {
296
312
  }
297
313
  return;
298
314
  }
299
- throw new Error(`Unknown pages command '${action}'. Use list, show, or upload.`);
315
+ throw new Error(`Unknown pages command '${action}'. Use list, show, runtime, or upload.`);
300
316
  }
301
317
  }
302
318
  return;
@@ -781,17 +797,14 @@ async function loadLocalSkillContext(directory, baseUrl) {
781
797
  };
782
798
  }
783
799
  async function loadLocalPageFiles(directory) {
784
- const filePlans = await collectFilePlans(directory, directory, { includeSkillMetadata: true });
800
+ const filePlans = await collectFilePlans(directory, directory, { includeSkillMetadata: true, pageProject: true });
785
801
  if (filePlans.length === 0) {
786
802
  throw new Error('The page directory is empty.');
787
803
  }
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);
804
+ const files = await preparePageFiles(directory, await collectFiles(filePlans));
792
805
  return {
793
- files: await collectFiles(filePlans),
794
- totalBytes,
806
+ files,
807
+ totalBytes: files.reduce((sum, file) => sum + file.content.length, 0),
795
808
  };
796
809
  }
797
810
  async function createSkillFromDirectory(client, localSkill, input) {
@@ -1328,6 +1341,66 @@ async function login(baseUrl) {
1328
1341
  });
1329
1342
  console.log(`Logged in as ${response.user.displayName} (${baseUrl})`);
1330
1343
  }
1344
+ const GCM_MINIMUM_VERSION = [2, 0, 931];
1345
+ async function configureGitCredentialManager(baseUrl) {
1346
+ baseUrl = normalizeBaseUrl(baseUrl);
1347
+ const versionResult = await runProcess('git', ['credential-manager', '--version'], { allowFailure: true });
1348
+ const version = parseVersion(`${versionResult.stdout} ${versionResult.stderr}`);
1349
+ if (versionResult.exitCode !== 0 || !version || compareVersions(version, GCM_MINIMUM_VERSION) < 0) {
1350
+ throw new Error('Git Credential Manager 2.0.931 or newer is required. Install or update GCM, then run this command again.');
1351
+ }
1352
+ await runProcess('git', ['credential-manager', 'configure']);
1353
+ const prefix = `credential.${baseUrl}`;
1354
+ const settings = [
1355
+ [`${prefix}.provider`, 'generic'],
1356
+ [`${prefix}.oauthClientId`, 'skillshare-gcm'],
1357
+ [`${prefix}.oauthRedirectUri`, 'http://127.0.0.1'],
1358
+ [`${prefix}.oauthAuthorizeEndpoint`, '/api/git/oauth/authorize'],
1359
+ [`${prefix}.oauthTokenEndpoint`, '/api/git/oauth/token'],
1360
+ [`${prefix}.oauthScopes`, 'git.read'],
1361
+ [`${prefix}.oauthDefaultUserName`, 'skillshare'],
1362
+ [`${prefix}.oauthUseClientAuthHeader`, 'false'],
1363
+ [`${prefix}.useHttpPath`, 'false'],
1364
+ ];
1365
+ for (const [key, value] of settings) {
1366
+ await runProcess('git', ['config', '--global', '--replace-all', key, value]);
1367
+ }
1368
+ console.log(`Registered ${baseUrl} in Git Credential Manager ${formatVersion(version)}.`);
1369
+ }
1370
+ async function unconfigureGitCredentialManager(baseUrl) {
1371
+ baseUrl = normalizeBaseUrl(baseUrl);
1372
+ const prefix = `credential.${baseUrl}`;
1373
+ for (const key of [
1374
+ 'provider',
1375
+ 'oauthClientId',
1376
+ 'oauthRedirectUri',
1377
+ 'oauthAuthorizeEndpoint',
1378
+ 'oauthTokenEndpoint',
1379
+ 'oauthScopes',
1380
+ 'oauthDefaultUserName',
1381
+ 'oauthUseClientAuthHeader',
1382
+ 'useHttpPath',
1383
+ ]) {
1384
+ await runProcess('git', ['config', '--global', '--unset-all', `${prefix}.${key}`], { allowFailure: true });
1385
+ }
1386
+ console.log(`Removed the Git Credential Manager registration for ${baseUrl}.`);
1387
+ }
1388
+ function parseVersion(value) {
1389
+ const match = value.match(/(?:^|\s)(\d+)\.(\d+)\.(\d+)(?:\D|$)/);
1390
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
1391
+ }
1392
+ function compareVersions(left, right) {
1393
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
1394
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
1395
+ if (difference !== 0) {
1396
+ return difference;
1397
+ }
1398
+ }
1399
+ return 0;
1400
+ }
1401
+ function formatVersion(version) {
1402
+ return version.join('.');
1403
+ }
1331
1404
  async function parseOptions(args) {
1332
1405
  const booleanFlags = new Set(['dry-run']);
1333
1406
  const flags = {};
@@ -1806,6 +1879,8 @@ async function collectFilePlans(root, current = root, options = {}) {
1806
1879
  const files = [];
1807
1880
  for (const entry of entries) {
1808
1881
  const entryPath = join(current, entry.name);
1882
+ if (options.pageProject && (entry.isSymbolicLink() || ['.git', '.celld', '.wrangler', 'node_modules'].includes(entry.name)))
1883
+ continue;
1809
1884
  if (entry.isDirectory()) {
1810
1885
  files.push(...(await collectFilePlans(root, entryPath, options)));
1811
1886
  continue;
package/dist/pages.js ADDED
@@ -0,0 +1,51 @@
1
+ import { builtinModules } from 'node:module';
2
+ import { build } from 'esbuild';
3
+ import { parse, printParseErrorCode } from 'jsonc-parser';
4
+ import { resolve, relative } from 'node:path';
5
+ export async function preparePageFiles(directory, files) {
6
+ const configurations = files.filter(file => file.relativePath === 'wrangler.json' || file.relativePath === 'wrangler.jsonc');
7
+ if (configurations.length > 1)
8
+ throw new Error('Publish only one of wrangler.json and wrangler.jsonc.');
9
+ const configFile = configurations[0];
10
+ if (!configFile) {
11
+ if (!files.some(file => file.relativePath === 'index.html'))
12
+ throw new Error('The page directory must contain index.html or a Wrangler configuration at its root.');
13
+ return files;
14
+ }
15
+ const errors = [];
16
+ const config = parse(configFile.content.toString('utf8'), errors, { allowTrailingComma: true });
17
+ if (errors.length || !config || typeof config !== 'object' || Array.isArray(config))
18
+ throw new Error(`Invalid ${configFile.relativePath}: ${errors.map(error => printParseErrorCode(error.error)).join(', ')}`);
19
+ if (!config.main || config.no_bundle === true)
20
+ return files;
21
+ if (typeof config.main !== 'string' || !files.some(file => file.relativePath === config.main.replace(/^\.\//, '')))
22
+ throw new Error('Wrangler main must name a file inside the uploaded project.');
23
+ if (files.some(file => file.relativePath.startsWith('__skillshare/')))
24
+ throw new Error('The __skillshare directory is reserved for the compiled Worker.');
25
+ const outputDirectory = resolve(directory, '__skillshare');
26
+ const output = await build({
27
+ absWorkingDir: directory,
28
+ entryPoints: [config.main],
29
+ outdir: outputDirectory,
30
+ entryNames: 'worker',
31
+ bundle: true,
32
+ write: false,
33
+ format: 'esm',
34
+ platform: 'browser',
35
+ target: 'es2024',
36
+ conditions: ['workerd', 'worker', 'browser'],
37
+ external: ['cloudflare:*', 'node:*', ...builtinModules],
38
+ loader: { '.wasm': 'copy' },
39
+ });
40
+ config.main = '__skillshare/worker.js';
41
+ // Native celld still performs its own validation/bundling, including sibling WASM modules.
42
+ const prepared = files.map(file => file === configFile
43
+ ? { ...file, content: Buffer.from(JSON.stringify(config, null, 2)) } : file);
44
+ for (const file of output.outputFiles)
45
+ prepared.push({
46
+ relativePath: relative(directory, file.path).replaceAll('\\', '/'),
47
+ content: Buffer.from(file.contents),
48
+ contentType: file.path.endsWith('.wasm') ? 'application/wasm' : 'text/javascript; charset=utf-8',
49
+ });
50
+ return prepared;
51
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@quadient/skillshare",
3
- "version": "1.3.0",
3
+ "version": "1.5.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",
@@ -29,6 +29,8 @@
29
29
  "node": ">=20"
30
30
  },
31
31
  "dependencies": {
32
- "@clack/prompts": "^1.7.0"
32
+ "@clack/prompts": "^1.7.0",
33
+ "esbuild": "0.28.2",
34
+ "jsonc-parser": "3.3.1"
33
35
  }
34
36
  }