@quadient/skillshare 1.8.1 → 1.10.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 +43 -1
- package/dist/agents-claude.js +274 -0
- package/dist/agents-codex.js +276 -0
- package/dist/agents-detect.js +147 -0
- package/dist/agents-process.js +50 -0
- package/dist/agents-session.js +267 -0
- package/dist/agents.js +230 -0
- package/dist/help.js +31 -15
- package/dist/index.js +79 -49
- package/dist/output.js +167 -0
- package/dist/pages.js +30 -1
- package/dist/teams.js +12 -4
- package/package.json +2 -2
package/dist/output.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Human-readable renderers for CLI commands. Every command also supports --json for scripts.
|
|
2
|
+
export function formatTable(headers, rows) {
|
|
3
|
+
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? '').length)));
|
|
4
|
+
const line = (cells) => cells
|
|
5
|
+
.map((cell, index) => (index === cells.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)))
|
|
6
|
+
.join(' ')
|
|
7
|
+
.trimEnd();
|
|
8
|
+
return [line(headers), ...rows.map(line)].join('\n');
|
|
9
|
+
}
|
|
10
|
+
export function formatDateTime(value) {
|
|
11
|
+
if (!value)
|
|
12
|
+
return '—';
|
|
13
|
+
const date = new Date(value);
|
|
14
|
+
if (Number.isNaN(date.getTime()))
|
|
15
|
+
return value;
|
|
16
|
+
const pad = (part) => String(part).padStart(2, '0');
|
|
17
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
18
|
+
}
|
|
19
|
+
export function formatByteSize(bytes) {
|
|
20
|
+
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
21
|
+
return '0 B';
|
|
22
|
+
}
|
|
23
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
24
|
+
let value = bytes;
|
|
25
|
+
let unitIndex = 0;
|
|
26
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
27
|
+
value /= 1024;
|
|
28
|
+
unitIndex += 1;
|
|
29
|
+
}
|
|
30
|
+
const decimals = value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
31
|
+
return `${value.toFixed(decimals)} ${units[unitIndex]}`;
|
|
32
|
+
}
|
|
33
|
+
function fields(entries) {
|
|
34
|
+
const present = entries.filter((entry) => entry[1] !== null && entry[1] !== undefined && entry[1] !== '');
|
|
35
|
+
const width = Math.max(...present.map(([label]) => label.length + 1));
|
|
36
|
+
return present.map(([label, value]) => `${`${label}:`.padEnd(width)} ${value}`);
|
|
37
|
+
}
|
|
38
|
+
export function plural(count, noun) {
|
|
39
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
40
|
+
}
|
|
41
|
+
function fileLines(files) {
|
|
42
|
+
const width = Math.max(0, ...files.map((file) => file.nameWithPath.length));
|
|
43
|
+
return files.map((file) => ` ${file.nameWithPath.padEnd(width)} ${formatByteSize(file.length)}`);
|
|
44
|
+
}
|
|
45
|
+
function limitedList(lines, limit, what) {
|
|
46
|
+
return lines.length > limit
|
|
47
|
+
? [...lines.slice(0, limit), ` … and ${lines.length - limit} more ${what} (use --json for all)`]
|
|
48
|
+
: lines;
|
|
49
|
+
}
|
|
50
|
+
export function renderUser(user, server) {
|
|
51
|
+
return [
|
|
52
|
+
`${user.displayName}${user.email ? ` <${user.email}>` : ''}`,
|
|
53
|
+
...fields([
|
|
54
|
+
['Server', server],
|
|
55
|
+
['Role', user.isAdmin ? 'admin' : 'user'],
|
|
56
|
+
['Microsoft account', user.entraObjectId ? 'linked' : 'not linked yet (open Skillshare in the browser once)'],
|
|
57
|
+
]),
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
export function renderSkillList(skills) {
|
|
61
|
+
if (skills.length === 0)
|
|
62
|
+
return 'No skills in the registry yet.';
|
|
63
|
+
return formatTable(['ID', 'NAME', 'LATEST', 'UPDATED'], skills.map((skill) => [
|
|
64
|
+
String(skill.id),
|
|
65
|
+
`${skill.name}${skill.deprecated ? ' (deprecated)' : ''}`,
|
|
66
|
+
skill.latestVersion ?? '—',
|
|
67
|
+
formatDateTime(skill.updated),
|
|
68
|
+
]));
|
|
69
|
+
}
|
|
70
|
+
export function renderSkillDetail(skill) {
|
|
71
|
+
const lines = [
|
|
72
|
+
`${skill.name} (#${skill.id})${skill.deprecated ? ' — deprecated' : ''}`,
|
|
73
|
+
skill.description,
|
|
74
|
+
'',
|
|
75
|
+
...fields([
|
|
76
|
+
['Owner', skill.ownerDisplayName],
|
|
77
|
+
['Latest', skill.latestVersion ?? 'no published version'],
|
|
78
|
+
['Updated', skill.updated ? formatDateTime(skill.updated) : undefined],
|
|
79
|
+
['Install', `skillshare install ${skill.id}`],
|
|
80
|
+
]),
|
|
81
|
+
];
|
|
82
|
+
if (skill.versions.length > 0) {
|
|
83
|
+
lines.push('', formatTable(['VERSION', 'PUBLISHED', 'BY', 'FILES', 'DOWNLOADS'], skill.versions.map((version) => [
|
|
84
|
+
`${version.version}${version.isLatest ? ' (latest)' : ''}`,
|
|
85
|
+
formatDateTime(version.created),
|
|
86
|
+
version.createdByDisplayName ?? '—',
|
|
87
|
+
String(version.files.length),
|
|
88
|
+
version.downloadCount === undefined ? '—' : String(version.downloadCount),
|
|
89
|
+
])));
|
|
90
|
+
}
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
export function renderSkillVersion(skillName, version) {
|
|
94
|
+
const totalBytes = version.files.reduce((sum, file) => sum + file.length, 0);
|
|
95
|
+
return [
|
|
96
|
+
`${skillName} ${version.version}${version.isLatest ? ' (latest)' : ''}`,
|
|
97
|
+
...fields([
|
|
98
|
+
['Published', `${formatDateTime(version.created)}${version.createdByDisplayName ? ` by ${version.createdByDisplayName}` : ''}`],
|
|
99
|
+
['Downloads', version.downloadCount],
|
|
100
|
+
]),
|
|
101
|
+
'',
|
|
102
|
+
`Files (${version.files.length}, ${formatByteSize(totalBytes)}):`,
|
|
103
|
+
...limitedList(fileLines(version.files), 50, 'files'),
|
|
104
|
+
].join('\n');
|
|
105
|
+
}
|
|
106
|
+
export function renderPageList(pages) {
|
|
107
|
+
if (pages.length === 0)
|
|
108
|
+
return 'No pages published yet.';
|
|
109
|
+
return formatTable(['SLUG', 'KIND', 'FILES', 'SIZE', 'OWNER', 'UPDATED'], pages.map((page) => [
|
|
110
|
+
page.slug,
|
|
111
|
+
page.kind === 'celld' ? 'app' : 'static',
|
|
112
|
+
String(page.fileCount),
|
|
113
|
+
formatByteSize(page.totalBytes),
|
|
114
|
+
page.ownerDisplayName,
|
|
115
|
+
formatDateTime(page.updated),
|
|
116
|
+
]));
|
|
117
|
+
}
|
|
118
|
+
export function renderPageDetail(page, baseUrl, now = new Date()) {
|
|
119
|
+
const month = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
120
|
+
const totalBytes = page.files.reduce((sum, file) => sum + file.length, 0);
|
|
121
|
+
return [
|
|
122
|
+
`${page.slug} (${page.kind === 'celld' ? 'application' : 'static site'})`,
|
|
123
|
+
...fields([
|
|
124
|
+
['URL', pageUrl(baseUrl, page.slug)],
|
|
125
|
+
['Owner', page.ownerDisplayName],
|
|
126
|
+
['Revision', `${page.contentRevision}, updated ${formatDateTime(page.updated)}`],
|
|
127
|
+
['Users this month', page.monthlyUsage.find((usage) => usage.month === month)?.uniqueUsers ?? 0],
|
|
128
|
+
]),
|
|
129
|
+
'',
|
|
130
|
+
`Files (${page.files.length}, ${formatByteSize(totalBytes)}):`,
|
|
131
|
+
...limitedList(fileLines(page.files), 30, 'files'),
|
|
132
|
+
].join('\n');
|
|
133
|
+
}
|
|
134
|
+
export function renderPageRuntime(slug, runtime, logLimit = 20) {
|
|
135
|
+
const lines = [
|
|
136
|
+
`${slug}: ${runtime.state} · revision ${runtime.revision}${runtime.processId ? ` · pid ${runtime.processId}` : ''}`,
|
|
137
|
+
];
|
|
138
|
+
if (runtime.error)
|
|
139
|
+
lines.push(`Error: ${runtime.error}`);
|
|
140
|
+
if (runtime.logs.length > 0) {
|
|
141
|
+
const shown = runtime.logs.slice(-logLimit);
|
|
142
|
+
lines.push('', `Last ${shown.length} of ${runtime.logs.length} log lines${runtime.logs.length > shown.length ? ' (use --json for all)' : ''}:`, ...shown);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
lines.push('No log output yet.');
|
|
146
|
+
}
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
export function renderTeamsStatus(status) {
|
|
150
|
+
const state = status.requiresReconnect
|
|
151
|
+
? 'reconnect required — run skillshare teams login'
|
|
152
|
+
: status.configured
|
|
153
|
+
? `connected${status.identity ? ` as ${status.identity}` : ''}`
|
|
154
|
+
: 'not connected — run skillshare teams login';
|
|
155
|
+
return [
|
|
156
|
+
`Teams: ${state}`,
|
|
157
|
+
...fields([
|
|
158
|
+
['Access', status.configured ? status.accessMode : undefined],
|
|
159
|
+
['Session expires', status.configured && status.expiresAt ? formatDateTime(status.expiresAt) : undefined],
|
|
160
|
+
['Updated', status.updatedAt ? formatDateTime(status.updatedAt) : undefined],
|
|
161
|
+
['MCP endpoint', status.endpoint],
|
|
162
|
+
]),
|
|
163
|
+
].join('\n');
|
|
164
|
+
}
|
|
165
|
+
export function pageUrl(baseUrl, slug) {
|
|
166
|
+
return `${baseUrl.replace(/\/+$/, '')}/p/${encodeURIComponent(slug)}/`;
|
|
167
|
+
}
|
package/dist/pages.js
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import { builtinModules } from 'node:module';
|
|
2
2
|
import { build } from 'esbuild';
|
|
3
3
|
import { parse, printParseErrorCode } from 'jsonc-parser';
|
|
4
|
-
import { resolve, relative } from 'node:path';
|
|
4
|
+
import { basename, dirname, resolve, relative } from 'node:path';
|
|
5
|
+
// Build output folders say nothing about the page, so publishing them under their own name is almost always a mistake.
|
|
6
|
+
const GENERIC_DIRECTORY_NAMES = new Set(['dist', 'build', 'out', 'output', 'public', 'www', 'static', 'site', '_site', 'html', 'web', 'export']);
|
|
7
|
+
const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
8
|
+
export function normalizePageSlug(value) {
|
|
9
|
+
const normalized = value.trim().toLowerCase();
|
|
10
|
+
if (!SLUG_PATTERN.test(normalized)) {
|
|
11
|
+
throw new Error('Page slug must be 1-64 lowercase letters, numbers, or hyphens, and cannot start or end with a hyphen.');
|
|
12
|
+
}
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
export function resolvePageSlug(directory, explicitSlug) {
|
|
16
|
+
if (explicitSlug !== undefined)
|
|
17
|
+
return normalizePageSlug(explicitSlug);
|
|
18
|
+
const resolvedDirectory = resolve(directory);
|
|
19
|
+
const name = basename(resolvedDirectory);
|
|
20
|
+
if (GENERIC_DIRECTORY_NAMES.has(name.toLowerCase().replace(/^\./, ''))) {
|
|
21
|
+
const suggestion = toSlugCandidate(basename(dirname(resolvedDirectory)));
|
|
22
|
+
throw new Error(`'${name}' looks like a build output folder, not a page name. Pass --slug, for example --slug ${suggestion && !GENERIC_DIRECTORY_NAMES.has(suggestion) ? suggestion : 'my-page'}.`);
|
|
23
|
+
}
|
|
24
|
+
const candidate = toSlugCandidate(name);
|
|
25
|
+
if (!candidate) {
|
|
26
|
+
throw new Error(`Cannot derive a page slug from '${name}'. Pass --slug <slug>.`);
|
|
27
|
+
}
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
function toSlugCandidate(value) {
|
|
31
|
+
const candidate = value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64).replace(/-+$/, '');
|
|
32
|
+
return SLUG_PATTERN.test(candidate) ? candidate : '';
|
|
33
|
+
}
|
|
5
34
|
export async function preparePageFiles(directory, files) {
|
|
6
35
|
const configurations = files.filter(file => file.relativePath === 'wrangler.json' || file.relativePath === 'wrangler.jsonc');
|
|
7
36
|
if (configurations.length > 1)
|
package/dist/teams.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Readable } from 'node:stream';
|
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { captureTeamsLogin } from './teams-auth.js';
|
|
8
8
|
import { watchTeamsConnection } from './teams-watch.js';
|
|
9
|
+
import { renderTeamsStatus } from './output.js';
|
|
9
10
|
const root = '/api/mcps/providers/teams';
|
|
10
11
|
const guid = /^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i;
|
|
11
12
|
/**
|
|
@@ -72,9 +73,15 @@ export async function runTeamsCommand(client, args, flags, appDirectory) {
|
|
|
72
73
|
}
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const json = flags.json === 'true';
|
|
77
|
+
if (action === 'status') {
|
|
78
|
+
const status = await client.get(`${root}/settings`);
|
|
79
|
+
console.log(json ? JSON.stringify(status, null, 2) : renderTeamsStatus(status));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (action === 'test') {
|
|
83
|
+
const result = await client.post(`${root}/test`);
|
|
84
|
+
console.log(json ? JSON.stringify(result, null, 2) : `Teams connection works${result.identity ? ` for ${result.identity}` : ''}.`);
|
|
78
85
|
return;
|
|
79
86
|
}
|
|
80
87
|
if (action === 'logout') {
|
|
@@ -107,7 +114,8 @@ export async function runTeamsCommand(client, args, flags, appDirectory) {
|
|
|
107
114
|
body: Readable.toWeb(stream), duplex: 'half',
|
|
108
115
|
});
|
|
109
116
|
await expectOk(response);
|
|
110
|
-
|
|
117
|
+
const item = await response.json();
|
|
118
|
+
console.log(json ? JSON.stringify(item, null, 2) : `Uploaded ${item.name ?? basename(path)} to your Teams files.${item.webUrl ? `\n ${item.webUrl}` : ''}`);
|
|
111
119
|
}
|
|
112
120
|
finally {
|
|
113
121
|
stream.destroy();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quadient/skillshare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Skillshare CLI for installing and publishing Quadient skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
17
17
|
"start": "node ./dist/index.js",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
|
-
"test": "tsx --test cli-args.test.ts install-files.test.ts pages.test.ts pages-dev.test.ts teams-auth.test.ts teams-watch.test.ts teams-browser.test.ts"
|
|
19
|
+
"test": "tsx --test cli-args.test.ts install-files.test.ts output.test.ts pages.test.ts pages-dev.test.ts teams-auth.test.ts teams-watch.test.ts teams-browser.test.ts agents.test.ts"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
22
22
|
"access": "public"
|