@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 +19 -0
- package/dist/index.js +87 -12
- package/dist/pages.js +51 -0
- package/package.json +5 -3
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(
|
|
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
|
-
|
|
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
|
|
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
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Skillshare CLI for installing and publishing Quadient skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"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
|
}
|