@quadient/skillshare 1.7.0 → 1.8.1
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 +12 -0
- package/dist/cli-args.js +86 -0
- package/dist/help.js +167 -0
- package/dist/index.js +288 -140
- package/dist/install-files.js +111 -0
- package/dist/teams-auth.js +1 -1
- package/dist/teams.js +16 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,6 +41,10 @@ skillshare teams access readOnly
|
|
|
41
41
|
skillshare teams logout
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
Starting with 1.8.1, Teams uses the Microsoft account object id that Skillshare records at
|
|
45
|
+
web sign-in, not the Skillshare user id. If the server does not know it yet, `teams login`
|
|
46
|
+
opens one more Skillshare sign-in in the browser to link the account.
|
|
47
|
+
|
|
44
48
|
Use `teams login local` for localhost or `--server <https-origin>` for another host.
|
|
45
49
|
`login` accepts `--server` too. Browser options: `--browser chrome|msedge|chromium`,
|
|
46
50
|
`--headless`, and `--browser-profile <dedicated-profile-directory>`.
|
|
@@ -50,6 +54,14 @@ permissions, Microsoft session lifetime and file-tool differences in the hosted
|
|
|
50
54
|
|
|
51
55
|
## Behavior
|
|
52
56
|
|
|
57
|
+
Starting with 1.8.0:
|
|
58
|
+
|
|
59
|
+
- `install` and `update` download into a staging directory first, so a failed transfer leaves the existing install untouched. Files that a new version no longer contains are removed.
|
|
60
|
+
- `installed.json` records a SHA-256 manifest of installed files. `update` skips installs with local changes and `install` refuses to overwrite them, or a directory that Skillshare did not install, unless you confirm interactively or pass `--force`. Installs recorded by older versions are not checked until they are updated once.
|
|
61
|
+
- Interactive mode shows the signed-in user and server, checks installed skills in parallel, and reports removed skills or failed checks as warnings instead of stopping.
|
|
62
|
+
- `login` prints the sign-in URL in case no browser opens, and stops waiting after 5 minutes.
|
|
63
|
+
- Unknown options are rejected with a suggestion. `skillshare <command> --help`, `skillshare help <command>` and `skillshare --version` are available.
|
|
64
|
+
|
|
53
65
|
- Starting with 1.6.2, `login`, `git configure` and API commands without a stored session default to `https://skillshare.quadientcloudpreprod.eu`. `login local` and `git configure local` still select `http://localhost:5050`.
|
|
54
66
|
- Existing sessions keep their saved server. After preprod cutover, run `skillshare login` again to replace a UAT session with a preprod session; stored tokens are not reused across hosts.
|
|
55
67
|
- Uploaded/installed records retain their registry URL. Legacy records without a URL still belong to UAT and are not silently reassigned to preprod.
|
package/dist/cli-args.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BOOLEAN_FLAGS, COMMANDS, GLOBAL_FLAGS, findCommand } from './help.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses options for a command. With a command name, unknown options are rejected
|
|
4
|
+
* before a value is read, so a typo gets a suggestion instead of a missing-value error.
|
|
5
|
+
*/
|
|
6
|
+
export function parseCliArgs(args, commandName) {
|
|
7
|
+
const flags = {};
|
|
8
|
+
const positionals = [];
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const current = args[index];
|
|
11
|
+
if (!current)
|
|
12
|
+
continue;
|
|
13
|
+
if (current === '-h') {
|
|
14
|
+
flags.help = 'true';
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!current.startsWith('--') || current === '--') {
|
|
18
|
+
positionals.push(current);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const separator = current.indexOf('=');
|
|
22
|
+
const key = separator > 0 ? current.slice(2, separator) : current.slice(2);
|
|
23
|
+
if (commandName) {
|
|
24
|
+
assertKnownFlag(commandName, key);
|
|
25
|
+
}
|
|
26
|
+
if (separator > 0) {
|
|
27
|
+
flags[key] = current.slice(separator + 1);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const value = args[index + 1];
|
|
31
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
32
|
+
// Booleans never consume a following positional, only an explicit true/false.
|
|
33
|
+
if (value === 'true' || value === 'false') {
|
|
34
|
+
flags[key] = value;
|
|
35
|
+
index += 1;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
flags[key] = 'true';
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (value === undefined || value.startsWith('--')) {
|
|
43
|
+
throw new Error(`Missing value for ${current}.`);
|
|
44
|
+
}
|
|
45
|
+
flags[key] = value;
|
|
46
|
+
index += 1;
|
|
47
|
+
}
|
|
48
|
+
return { flags, positionals };
|
|
49
|
+
}
|
|
50
|
+
function assertKnownFlag(commandName, name) {
|
|
51
|
+
const command = findCommand(commandName);
|
|
52
|
+
if (!command)
|
|
53
|
+
return;
|
|
54
|
+
const allowed = [...GLOBAL_FLAGS, ...command.flags];
|
|
55
|
+
if (allowed.includes(name))
|
|
56
|
+
return;
|
|
57
|
+
const suggestion = suggest(name, allowed);
|
|
58
|
+
throw new Error(`Unknown option --${name} for "${command.name}".${suggestion ? ` Did you mean --${suggestion}?` : ''} Run "skillshare ${command.name} --help" for options.`);
|
|
59
|
+
}
|
|
60
|
+
export function formatUnknownCommand(name) {
|
|
61
|
+
const suggestion = suggest(name, COMMANDS.map((command) => command.name));
|
|
62
|
+
return `Unknown command '${name}'.${suggestion ? ` Did you mean '${suggestion}'?` : ''} Run "skillshare --help" for the list of commands.`;
|
|
63
|
+
}
|
|
64
|
+
export function suggest(input, candidates) {
|
|
65
|
+
let best = null;
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
const distance = candidate.startsWith(input) || input.startsWith(candidate) ? 1 : levenshtein(input, candidate);
|
|
68
|
+
if (distance <= 2 && (!best || distance < best.distance)) {
|
|
69
|
+
best = { candidate, distance };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return best?.candidate ?? null;
|
|
73
|
+
}
|
|
74
|
+
function levenshtein(left, right) {
|
|
75
|
+
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
76
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
77
|
+
let diagonal = previous[0];
|
|
78
|
+
previous[0] = i;
|
|
79
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
80
|
+
const above = previous[j];
|
|
81
|
+
previous[j] = Math.min(above + 1, previous[j - 1] + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1));
|
|
82
|
+
diagonal = above;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return previous[right.length];
|
|
86
|
+
}
|
package/dist/help.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
export const GLOBAL_FLAGS = ['token', 'server', 'help'];
|
|
3
|
+
export const BOOLEAN_FLAGS = new Set(['dry-run', 'force', 'headless', 'help']);
|
|
4
|
+
export const COMMANDS = [
|
|
5
|
+
{
|
|
6
|
+
name: 'login',
|
|
7
|
+
summary: 'Sign in through the browser and store a CLI session',
|
|
8
|
+
usage: ['login [local] [--server <url>]'],
|
|
9
|
+
description: 'Opens the Skillshare sign-in page. If no browser opens, copy the printed URL into one.',
|
|
10
|
+
examples: ['skillshare login', 'skillshare login local'],
|
|
11
|
+
flags: [],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'logout',
|
|
15
|
+
summary: 'Remove the stored CLI session',
|
|
16
|
+
usage: ['logout'],
|
|
17
|
+
flags: [],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'whoami',
|
|
21
|
+
summary: 'Show the signed-in user',
|
|
22
|
+
usage: ['whoami'],
|
|
23
|
+
flags: [],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'list',
|
|
27
|
+
summary: 'List skills in the registry',
|
|
28
|
+
usage: ['list'],
|
|
29
|
+
flags: [],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'show',
|
|
33
|
+
summary: 'Show a skill or one of its versions',
|
|
34
|
+
usage: ['show <skillId> [version]'],
|
|
35
|
+
flags: [],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'install',
|
|
39
|
+
summary: 'Install a skill for your coding agents',
|
|
40
|
+
usage: [
|
|
41
|
+
'install [skillId|skillName] [version]',
|
|
42
|
+
'install <skillId|skillName> [version] --target <directory> [--force]',
|
|
43
|
+
'install --skill-version-id <id> --target <directory> [--force]',
|
|
44
|
+
],
|
|
45
|
+
description: 'Without --target the CLI asks for the version, scope and agents. Files that you changed locally are not overwritten unless you confirm or pass --force.',
|
|
46
|
+
options: [
|
|
47
|
+
['--target <directory>', 'Install into this directory instead of agent skill folders'],
|
|
48
|
+
['--skill-version-id <id>', 'Install an exact published skill version'],
|
|
49
|
+
['--force', 'Overwrite local changes in the target directory'],
|
|
50
|
+
],
|
|
51
|
+
examples: ['skillshare install', 'skillshare install "Prompt Lint"', 'skillshare install 12 0.1.0 --target ./installed'],
|
|
52
|
+
flags: ['target', 'skill-version-id', 'force'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'update',
|
|
56
|
+
summary: 'Update installed skills to their latest versions',
|
|
57
|
+
usage: ['update [skillId|skillName] [--target <directory>] [--dry-run] [--force]'],
|
|
58
|
+
description: 'Removes files that the new version no longer contains. Installs with local changes are skipped unless you pass --force.',
|
|
59
|
+
options: [
|
|
60
|
+
['--target <directory>', 'Only update the install in this directory'],
|
|
61
|
+
['--dry-run', 'Print planned changes without writing files'],
|
|
62
|
+
['--force', 'Overwrite local changes'],
|
|
63
|
+
],
|
|
64
|
+
examples: ['skillshare update', 'skillshare update "Prompt Lint" --dry-run'],
|
|
65
|
+
flags: ['target', 'dry-run', 'force'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'upload',
|
|
69
|
+
summary: 'Create a skill or publish a new version from a directory',
|
|
70
|
+
usage: ['upload <directory> --version <version> [--skill-id <id>] [--name <name>] [--description <text>]'],
|
|
71
|
+
options: [
|
|
72
|
+
['--version <version>', 'Version to publish'],
|
|
73
|
+
['--skill-id <id>', 'Publish to this existing skill'],
|
|
74
|
+
['--name <name>', 'Name for a new skill (defaults to SKILL.md)'],
|
|
75
|
+
['--description <text>', 'Description for a new skill (defaults to SKILL.md)'],
|
|
76
|
+
],
|
|
77
|
+
examples: ['skillshare upload ./prompt-lint --version 0.2.0'],
|
|
78
|
+
flags: ['version', 'skill-id', 'name', 'description'],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'pages',
|
|
82
|
+
summary: 'Publish and inspect authenticated pages',
|
|
83
|
+
usage: [
|
|
84
|
+
'pages list',
|
|
85
|
+
'pages show <slug>',
|
|
86
|
+
'pages runtime <slug>',
|
|
87
|
+
'pages upload <directory> [--slug <slug>]',
|
|
88
|
+
'pages dev [directory] [--slug <slug>] [--port <port>] [--build <command> --watch-dir <sources>]',
|
|
89
|
+
' [--celld <executable>] [--user-id <id>] [--user-name <name>] [--user-email <email>]',
|
|
90
|
+
],
|
|
91
|
+
examples: ['skillshare pages upload ./site --slug release-notes', 'skillshare pages dev . --slug my-app'],
|
|
92
|
+
flags: ['slug'],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'teams',
|
|
96
|
+
summary: 'Connect Microsoft Teams to the Skillshare Teams MCP',
|
|
97
|
+
usage: [
|
|
98
|
+
'teams login [local] [--browser chrome|msedge|chromium] [--headless] [--browser-profile <directory>]',
|
|
99
|
+
'teams watch [local] [--browser chrome|msedge|chromium]',
|
|
100
|
+
'teams status | test | logout',
|
|
101
|
+
'teams access readOnly|fullAccess',
|
|
102
|
+
'teams upload <file>',
|
|
103
|
+
'teams download <url> --output <new-file>',
|
|
104
|
+
],
|
|
105
|
+
flags: ['browser', 'headless', 'browser-profile', 'output'],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'git',
|
|
109
|
+
summary: 'Register Skillshare in Git Credential Manager',
|
|
110
|
+
usage: ['git configure [local]', 'git unconfigure [local]'],
|
|
111
|
+
flags: [],
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
const COMMAND_ALIASES = { page: 'pages' };
|
|
115
|
+
export function findCommand(name) {
|
|
116
|
+
if (!name)
|
|
117
|
+
return undefined;
|
|
118
|
+
const resolved = COMMAND_ALIASES[name] ?? name;
|
|
119
|
+
return COMMANDS.find((command) => command.name === resolved);
|
|
120
|
+
}
|
|
121
|
+
export function renderUsage(defaultUrl) {
|
|
122
|
+
const width = Math.max(...COMMANDS.map((command) => command.name.length));
|
|
123
|
+
return [
|
|
124
|
+
'Usage: skillshare [command] [options]',
|
|
125
|
+
'',
|
|
126
|
+
'Run without a command for the interactive mode.',
|
|
127
|
+
'',
|
|
128
|
+
'Commands:',
|
|
129
|
+
...COMMANDS.map((command) => ` ${command.name.padEnd(width)} ${command.summary}`),
|
|
130
|
+
'',
|
|
131
|
+
'Options:',
|
|
132
|
+
' --token <token> Bearer token for API requests (overrides SKILLSHARE_TOKEN)',
|
|
133
|
+
' --server <url> Skillshare origin for login and Teams commands',
|
|
134
|
+
' -h, --help Show help for a command',
|
|
135
|
+
' -v, --version Show the CLI version',
|
|
136
|
+
'',
|
|
137
|
+
`Default server: ${defaultUrl}`,
|
|
138
|
+
'A stored session keeps its server; run "skillshare login" to switch to the default.',
|
|
139
|
+
'Run "skillshare <command> --help" for details.',
|
|
140
|
+
].join('\n');
|
|
141
|
+
}
|
|
142
|
+
export function renderCommandHelp(command) {
|
|
143
|
+
const lines = [`${command.summary}.`, '', 'Usage:', ...command.usage.map((usage) => ` skillshare ${usage}`)];
|
|
144
|
+
if (command.description)
|
|
145
|
+
lines.push('', command.description);
|
|
146
|
+
if (command.options?.length) {
|
|
147
|
+
const width = Math.max(...command.options.map(([flag]) => flag.length));
|
|
148
|
+
lines.push('', 'Options:', ...command.options.map(([flag, text]) => ` ${flag.padEnd(width)} ${text}`));
|
|
149
|
+
}
|
|
150
|
+
if (command.examples?.length)
|
|
151
|
+
lines.push('', 'Examples:', ...command.examples.map((example) => ` ${example}`));
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
|
154
|
+
export function readCliVersion() {
|
|
155
|
+
// dist/index.js and the source index.ts sit at different depths below package.json.
|
|
156
|
+
for (const candidate of ['../package.json', './package.json']) {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(readFileSync(new URL(candidate, import.meta.url), 'utf8'));
|
|
159
|
+
if (parsed.name === '@quadient/skillshare' && parsed.version)
|
|
160
|
+
return parsed.version;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Try the next location.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return 'unknown';
|
|
167
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { assertCredentialDestination, runTeamsCommand } from './teams.js';
|
|
1
|
+
import { assertCredentialDestination, resolveTeamsObjectId, runTeamsCommand } from './teams.js';
|
|
2
|
+
import { formatUnknownCommand, parseCliArgs } from './cli-args.js';
|
|
3
|
+
import { findCommand, readCliVersion, renderCommandHelp, renderUsage } from './help.js';
|
|
4
|
+
import { inspectInstallTarget, installFilesAtomically } from './install-files.js';
|
|
2
5
|
import { preparePageFiles } from './pages.js';
|
|
3
6
|
import { runPageDev } from './pages-dev.js';
|
|
4
7
|
import { createServer } from 'node:http';
|
|
@@ -6,9 +9,9 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
6
9
|
import { spawn } from 'node:child_process';
|
|
7
10
|
import { existsSync } from 'node:fs';
|
|
8
11
|
import { mkdir, open, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
9
|
-
import { basename,
|
|
12
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
10
13
|
import { homedir } from 'node:os';
|
|
11
|
-
import { cancel, confirm, intro, isCancel, multiselect, outro, progress, select, spinner, text } from '@clack/prompts';
|
|
14
|
+
import { cancel, confirm, intro, isCancel, log, multiselect, outro, progress, select, spinner, text } from '@clack/prompts';
|
|
12
15
|
const DEFAULT_URL = 'https://skillshare.quadientcloudpreprod.eu';
|
|
13
16
|
// Records without a registry URL were created against UAT before the preprod migration.
|
|
14
17
|
const LEGACY_UAT_URL = 'https://skillshare.inspireclouduat.net';
|
|
@@ -68,45 +71,7 @@ const AGENTS = [
|
|
|
68
71
|
{ value: 'pochi', label: 'Pochi', projectPath: '.pochi/skills', globalPath: '.pochi/skills' },
|
|
69
72
|
{ value: 'adal', label: 'AdaL', projectPath: '.adal/skills', globalPath: '.adal/skills' },
|
|
70
73
|
];
|
|
71
|
-
const
|
|
72
|
-
skillshare <command> [options]
|
|
73
|
-
|
|
74
|
-
Commands:
|
|
75
|
-
login [local]
|
|
76
|
-
logout
|
|
77
|
-
whoami
|
|
78
|
-
list
|
|
79
|
-
show <skillId> [version]
|
|
80
|
-
upload <directory> --version <version> [--skill-id <id>] [--name <name> --description <desc>]
|
|
81
|
-
install
|
|
82
|
-
install <skillId|skillName> [version]
|
|
83
|
-
install <skillId|skillName> [version] --target <directory>
|
|
84
|
-
install --skill-version-id <id> --target <directory>
|
|
85
|
-
update [skillId|skillName] [--target <directory>] [--dry-run]
|
|
86
|
-
pages list
|
|
87
|
-
pages show <slug>
|
|
88
|
-
pages upload <directory> [--slug <slug>]
|
|
89
|
-
pages dev [directory] [--slug <slug>] [--port <port>]
|
|
90
|
-
[--build <command> --watch-dir <sources>] [--celld <executable>]
|
|
91
|
-
[--user-id <id>] [--user-name <name>] [--user-email <email>]
|
|
92
|
-
teams login [local] [--browser chrome|msedge|chromium] [--headless]
|
|
93
|
-
teams watch [local] [--browser chrome|msedge|chromium]
|
|
94
|
-
teams logout
|
|
95
|
-
teams status
|
|
96
|
-
teams test
|
|
97
|
-
teams access readOnly|fullAccess
|
|
98
|
-
teams upload <file>
|
|
99
|
-
teams download <url> --output <new-file>
|
|
100
|
-
git configure [local]
|
|
101
|
-
git unconfigure [local]
|
|
102
|
-
|
|
103
|
-
Options:
|
|
104
|
-
--token <token> Bearer token for API requests (overrides SKILLSHARE_TOKEN)
|
|
105
|
-
--server <url> Skillshare origin for login and Teams commands
|
|
106
|
-
|
|
107
|
-
Default server: ${DEFAULT_URL}
|
|
108
|
-
A stored session keeps its server; run "skillshare login" to switch to the default.
|
|
109
|
-
`;
|
|
74
|
+
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
110
75
|
async function main() {
|
|
111
76
|
const args = process.argv.slice(2);
|
|
112
77
|
const command = args[0];
|
|
@@ -114,11 +79,29 @@ async function main() {
|
|
|
114
79
|
await runInteractive();
|
|
115
80
|
return;
|
|
116
81
|
}
|
|
117
|
-
if (command === '--
|
|
118
|
-
console.log(
|
|
119
|
-
|
|
82
|
+
if (command === '--version' || command === '-v') {
|
|
83
|
+
console.log(readCliVersion());
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
87
|
+
const topic = command === 'help' ? findCommand(args[1]) : undefined;
|
|
88
|
+
if (command === 'help' && args[1] && !topic) {
|
|
89
|
+
throw new Error(formatUnknownCommand(args[1]));
|
|
90
|
+
}
|
|
91
|
+
console.log(topic ? renderCommandHelp(topic) : renderUsage(DEFAULT_URL));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const commandSpec = findCommand(command);
|
|
95
|
+
if (!commandSpec) {
|
|
96
|
+
throw new Error(formatUnknownCommand(command));
|
|
97
|
+
}
|
|
98
|
+
// pages dev validates its own options.
|
|
99
|
+
const isPageDev = (command === 'pages' || command === 'page') && args[1] === 'dev';
|
|
100
|
+
const options = await parseOptions(args.slice(1), isPageDev ? undefined : command);
|
|
101
|
+
if (isFlagEnabled(options.flags, 'help')) {
|
|
102
|
+
console.log(renderCommandHelp(commandSpec));
|
|
103
|
+
return;
|
|
120
104
|
}
|
|
121
|
-
const options = await parseOptions(args.slice(1));
|
|
122
105
|
if ((command === 'pages' || command === 'page') && options.positionals[0] === 'dev') {
|
|
123
106
|
await runPageDev(options.positionals[1] ?? '.', options.flags);
|
|
124
107
|
return;
|
|
@@ -139,7 +122,15 @@ async function main() {
|
|
|
139
122
|
assertCredentialDestination(baseUrl);
|
|
140
123
|
if (!options.token && !await loadSession(baseUrl))
|
|
141
124
|
await login(baseUrl);
|
|
142
|
-
|
|
125
|
+
const teamsClient = await createClient(baseUrl, options.token);
|
|
126
|
+
const action = options.positionals[0];
|
|
127
|
+
if (!options.token && (action === 'login' || action === 'watch')
|
|
128
|
+
&& !resolveTeamsObjectId(await teamsClient.get('/api/auth/me'))) {
|
|
129
|
+
// A browser sign-in lets the server record the Entra object id for this account.
|
|
130
|
+
console.log('Linking your Microsoft account to Skillshare. Sign in once more in the browser.');
|
|
131
|
+
await login(baseUrl);
|
|
132
|
+
}
|
|
133
|
+
await runTeamsCommand(teamsClient, options.positionals, options.flags, APP_DIR);
|
|
143
134
|
return;
|
|
144
135
|
}
|
|
145
136
|
case 'git': {
|
|
@@ -223,6 +214,7 @@ async function main() {
|
|
|
223
214
|
const skillRef = options.positionals[0];
|
|
224
215
|
const version = options.positionals[1];
|
|
225
216
|
const target = options.flags.target;
|
|
217
|
+
const force = isFlagEnabled(options.flags, 'force');
|
|
226
218
|
if (skillVersionId) {
|
|
227
219
|
if (!target) {
|
|
228
220
|
throw new Error('install with --skill-version-id requires --target <directory>');
|
|
@@ -233,6 +225,7 @@ async function main() {
|
|
|
233
225
|
}
|
|
234
226
|
const detail = await client.get(`/api/skill-versions/${parsedSkillVersionId}`);
|
|
235
227
|
const skill = await client.get(`/api/skills/${detail.skillId}`);
|
|
228
|
+
await ensureTargetsWritable(client.baseUrl, [target], { force });
|
|
236
229
|
await client.post(`/api/skills/${detail.skillId}/versions/${detail.version}/downloads`);
|
|
237
230
|
const installProgress = createInstallProgressReporter({
|
|
238
231
|
skillName: skill.name,
|
|
@@ -262,6 +255,7 @@ async function main() {
|
|
|
262
255
|
await runInteractiveInstall(client, {
|
|
263
256
|
skillRef,
|
|
264
257
|
version,
|
|
258
|
+
force,
|
|
265
259
|
});
|
|
266
260
|
return;
|
|
267
261
|
}
|
|
@@ -275,6 +269,7 @@ async function main() {
|
|
|
275
269
|
throw new Error(`Skill '${skill.name}' does not have a published version.`);
|
|
276
270
|
}
|
|
277
271
|
const detail = await client.get(`/api/skills/${skill.id}/versions/${effectiveVersion}`);
|
|
272
|
+
await ensureTargetsWritable(client.baseUrl, [target], { force });
|
|
278
273
|
await client.post(`/api/skills/${skill.id}/versions/${effectiveVersion}/downloads`);
|
|
279
274
|
const installProgress = createInstallProgressReporter({
|
|
280
275
|
skillName: skill.name,
|
|
@@ -305,6 +300,7 @@ async function main() {
|
|
|
305
300
|
skillRef: options.positionals[0],
|
|
306
301
|
target: options.flags.target,
|
|
307
302
|
dryRun: isFlagEnabled(options.flags, 'dry-run'),
|
|
303
|
+
force: isFlagEnabled(options.flags, 'force'),
|
|
308
304
|
});
|
|
309
305
|
return;
|
|
310
306
|
}
|
|
@@ -351,23 +347,22 @@ async function main() {
|
|
|
351
347
|
return;
|
|
352
348
|
}
|
|
353
349
|
default:
|
|
354
|
-
throw new Error(
|
|
350
|
+
throw new Error(formatUnknownCommand(command));
|
|
355
351
|
}
|
|
356
352
|
}
|
|
357
353
|
async function runInteractive() {
|
|
358
|
-
intro(
|
|
354
|
+
intro(`Skillshare ${readCliVersion()}`);
|
|
359
355
|
const baseUrl = await resolveActiveBaseUrl();
|
|
360
|
-
await ensureInteractiveLogin(baseUrl);
|
|
356
|
+
const user = await ensureInteractiveLogin(baseUrl);
|
|
361
357
|
const client = await createClient(baseUrl);
|
|
362
|
-
|
|
358
|
+
log.info(`Signed in as ${user.displayName}${user.email ? ` <${user.email}>` : ''} · ${new URL(client.baseUrl).host}`);
|
|
359
|
+
const pendingInstalledUpdates = await checkInstalledSkillsForUpdates(client);
|
|
363
360
|
if (pendingInstalledUpdates.length > 0) {
|
|
364
361
|
const shouldUpdateInstalled = await promptConfirm(pendingInstalledUpdates.length === 1
|
|
365
362
|
? `There is 1 installed skill with a newer version available. Update it now?`
|
|
366
363
|
: `There are ${pendingInstalledUpdates.length} installed skills with newer versions available. Update all of them now?`, true);
|
|
367
364
|
if (shouldUpdateInstalled) {
|
|
368
365
|
await updateInstalledSkills(client, { candidates: pendingInstalledUpdates });
|
|
369
|
-
outro('Done.');
|
|
370
|
-
return;
|
|
371
366
|
}
|
|
372
367
|
}
|
|
373
368
|
if (existsSync(join(process.cwd(), SKILL_FILE_NAME))) {
|
|
@@ -387,12 +382,17 @@ async function runInteractive() {
|
|
|
387
382
|
{ value: 'install', label: 'Install a skill', hint: 'Browse registry and install to agents' },
|
|
388
383
|
{ value: 'update', label: 'Update installed skills', hint: 'Refresh recorded installs to the latest registry versions' },
|
|
389
384
|
{ value: 'upload', label: 'Upload a skill', hint: 'Create a new skill or publish a version' },
|
|
385
|
+
{ value: 'exit', label: 'Exit' },
|
|
390
386
|
],
|
|
391
387
|
});
|
|
392
388
|
if (isCancel(action)) {
|
|
393
389
|
cancel('Cancelled.');
|
|
394
390
|
process.exit(0);
|
|
395
391
|
}
|
|
392
|
+
if (action === 'exit') {
|
|
393
|
+
outro('Bye.');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
396
|
if (action === 'install') {
|
|
397
397
|
await runInteractiveInstall(client);
|
|
398
398
|
}
|
|
@@ -404,22 +404,53 @@ async function runInteractive() {
|
|
|
404
404
|
}
|
|
405
405
|
outro('Done.');
|
|
406
406
|
}
|
|
407
|
+
async function checkInstalledSkillsForUpdates(client) {
|
|
408
|
+
const records = (await readInstalledSkillRecords()).filter((record) => record.baseUrl === client.baseUrl);
|
|
409
|
+
if (records.length === 0) {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
const status = spinner();
|
|
413
|
+
status.start(`Checking ${records.length} installed skill${records.length === 1 ? '' : 's'} for updates`);
|
|
414
|
+
const problems = [];
|
|
415
|
+
try {
|
|
416
|
+
const candidates = await collectInstalledSkillUpdateCandidates(client, {
|
|
417
|
+
records,
|
|
418
|
+
onSkipped(skip) {
|
|
419
|
+
if (skip.reason === 'removed' || skip.reason === 'error') {
|
|
420
|
+
problems.push(skip.message);
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
status.stop(candidates.length > 0
|
|
425
|
+
? `${candidates.length} installed skill${candidates.length === 1 ? ' has' : 's have'} a newer version`
|
|
426
|
+
: 'Installed skills are up to date');
|
|
427
|
+
if (problems.length > 0) {
|
|
428
|
+
log.warn(problems.join('\n'));
|
|
429
|
+
}
|
|
430
|
+
return candidates;
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
// The update check is optional; never let it block the main menu.
|
|
434
|
+
status.stop('Could not check installed skills for updates');
|
|
435
|
+
log.warn(error instanceof Error ? error.message : String(error));
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
407
439
|
async function ensureInteractiveLogin(baseUrl) {
|
|
408
440
|
const session = await loadSession(baseUrl);
|
|
409
|
-
if (!session) {
|
|
410
|
-
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
if (new Date(session.refreshTokenExpiresAt).getTime() <= Date.now()) {
|
|
414
|
-
await login(baseUrl);
|
|
415
|
-
return;
|
|
441
|
+
if (!session || new Date(session.refreshTokenExpiresAt).getTime() <= Date.now()) {
|
|
442
|
+
return login(baseUrl);
|
|
416
443
|
}
|
|
417
444
|
try {
|
|
418
445
|
const client = await createClient(baseUrl);
|
|
419
|
-
await client.get('/api/auth/me');
|
|
446
|
+
return await client.get('/api/auth/me');
|
|
420
447
|
}
|
|
421
|
-
catch {
|
|
422
|
-
|
|
448
|
+
catch (error) {
|
|
449
|
+
// Offline is not an expired session; signing in again would not help.
|
|
450
|
+
if (error instanceof NetworkError) {
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
return login(baseUrl);
|
|
423
454
|
}
|
|
424
455
|
}
|
|
425
456
|
async function runInteractiveInstall(client, options) {
|
|
@@ -440,9 +471,13 @@ async function runInteractiveInstall(client, options) {
|
|
|
440
471
|
throw new Error('Select at least one agent.');
|
|
441
472
|
}
|
|
442
473
|
const versionDetail = await client.get(`/api/skills/${skill.id}/versions/${selectedVersion}`);
|
|
443
|
-
await client.post(`/api/skills/${skill.id}/versions/${selectedVersion}/downloads`);
|
|
444
474
|
const installName = createInstalledSkillDirectoryName(skill);
|
|
445
475
|
const installTargets = resolveInstallTargets(selectedAgents, scope, installName);
|
|
476
|
+
await ensureTargetsWritable(client.baseUrl, installTargets.map((target) => target.targetRoot), {
|
|
477
|
+
force: options?.force,
|
|
478
|
+
interactive: true,
|
|
479
|
+
});
|
|
480
|
+
await client.post(`/api/skills/${skill.id}/versions/${selectedVersion}/downloads`);
|
|
446
481
|
const installProgress = createInstallProgressReporter({
|
|
447
482
|
skillName: skill.name,
|
|
448
483
|
version: selectedVersion,
|
|
@@ -466,7 +501,11 @@ async function runInteractiveInstall(client, options) {
|
|
|
466
501
|
installProgress.clear();
|
|
467
502
|
throw error;
|
|
468
503
|
}
|
|
469
|
-
|
|
504
|
+
log.success([
|
|
505
|
+
`Installed ${skill.name}@${selectedVersion} for ${selectedAgents.length} agent${selectedAgents.length === 1 ? '' : 's'}:`,
|
|
506
|
+
...installTargets.map((target) => ` ${formatInstallTargetRoot(target.targetRoot)}`),
|
|
507
|
+
'Restart running agents to pick up the skill.',
|
|
508
|
+
].join('\n'));
|
|
470
509
|
}
|
|
471
510
|
async function runInteractiveUpload(client, initialDirectory) {
|
|
472
511
|
const directory = initialDirectory
|
|
@@ -524,6 +563,7 @@ async function updateInstalledSkills(client, options) {
|
|
|
524
563
|
const targetFilter = normalizeText(options?.target);
|
|
525
564
|
const normalizedTargetFilter = targetFilter ? resolve(targetFilter) : null;
|
|
526
565
|
const dryRun = options?.dryRun ?? false;
|
|
566
|
+
const force = options?.force ?? false;
|
|
527
567
|
let installedRecords = (await readInstalledSkillRecords())
|
|
528
568
|
.filter((record) => record.baseUrl === client.baseUrl);
|
|
529
569
|
if (skillRef) {
|
|
@@ -547,11 +587,11 @@ async function updateInstalledSkills(client, options) {
|
|
|
547
587
|
const candidates = options?.candidates
|
|
548
588
|
?? await collectInstalledSkillUpdateCandidates(client, {
|
|
549
589
|
records: installedRecords,
|
|
550
|
-
onSkipped(
|
|
551
|
-
console.log(message);
|
|
590
|
+
onSkipped(skip) {
|
|
591
|
+
console.log(skip.message);
|
|
552
592
|
},
|
|
553
593
|
});
|
|
554
|
-
|
|
594
|
+
let skippedCount = installedRecords.length - candidates.length;
|
|
555
595
|
if (candidates.length === 0) {
|
|
556
596
|
if (dryRun) {
|
|
557
597
|
console.log(`Dry run finished. Would update 0, skipped ${skippedCount}.`);
|
|
@@ -565,9 +605,16 @@ async function updateInstalledSkills(client, options) {
|
|
|
565
605
|
let plannedCount = 0;
|
|
566
606
|
for (const candidate of candidates) {
|
|
567
607
|
const { record, detail, latestVersion } = candidate;
|
|
608
|
+
const targetState = force ? null : await inspectInstallTarget(record.targetRoot, record);
|
|
609
|
+
if (targetState?.state === 'modified') {
|
|
610
|
+
skippedCount += 1;
|
|
611
|
+
console.log(`Skipping ${detail.name} in ${formatInstallTargetRoot(record.targetRoot)}: local changes in ${formatChangedPaths(targetState.paths)}. `
|
|
612
|
+
+ `Run "skillshare update ${record.skillId} --target ${formatCliPath(record.targetRoot)} --force" to overwrite them.`);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
568
615
|
if (dryRun) {
|
|
569
616
|
plannedCount += 1;
|
|
570
|
-
console.log(`Would update ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${record.targetRoot}`);
|
|
617
|
+
console.log(`Would update ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${formatInstallTargetRoot(record.targetRoot)}`);
|
|
571
618
|
continue;
|
|
572
619
|
}
|
|
573
620
|
const versionDetail = await client.get(`/api/skills/${record.skillId}/versions/${latestVersion}`);
|
|
@@ -594,7 +641,7 @@ async function updateInstalledSkills(client, options) {
|
|
|
594
641
|
throw error;
|
|
595
642
|
}
|
|
596
643
|
updatedCount += 1;
|
|
597
|
-
console.log(`Updated ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${record.targetRoot}`);
|
|
644
|
+
console.log(`Updated ${detail.name} from ${record.skillVersion} to ${latestVersion} in ${formatInstallTargetRoot(record.targetRoot)}`);
|
|
598
645
|
}
|
|
599
646
|
if (dryRun) {
|
|
600
647
|
console.log(`Dry run finished. Would update ${plannedCount}, skipped ${skippedCount}.`);
|
|
@@ -605,30 +652,59 @@ async function updateInstalledSkills(client, options) {
|
|
|
605
652
|
async function collectInstalledSkillUpdateCandidates(client, options) {
|
|
606
653
|
const records = options?.records
|
|
607
654
|
?? (await readInstalledSkillRecords()).filter((record) => record.baseUrl === client.baseUrl);
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
655
|
+
const details = new Map();
|
|
656
|
+
// Each record is checked independently, so one removed skill or failed request does not hide the others.
|
|
657
|
+
const results = await mapWithConcurrency(records, 6, async (record) => {
|
|
658
|
+
const skillId = record.skillId;
|
|
659
|
+
if (skillId == null) {
|
|
660
|
+
return { record, reason: 'no-id', message: `Skipping ${record.skillName}: installed record in ${record.targetRoot} does not contain a skill id.` };
|
|
661
|
+
}
|
|
662
|
+
let detail;
|
|
663
|
+
try {
|
|
664
|
+
if (!details.has(skillId)) {
|
|
665
|
+
details.set(skillId, client.get(`/api/skills/${skillId}`));
|
|
666
|
+
}
|
|
667
|
+
detail = await details.get(skillId);
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
671
|
+
return { record, reason: 'removed', message: `${record.skillName || `Skill ${skillId}`} in ${formatInstallTargetRoot(record.targetRoot)} no longer exists in the registry.` };
|
|
672
|
+
}
|
|
673
|
+
return { record, reason: 'error', message: `Could not check ${record.skillName || `skill ${skillId}`}: ${error instanceof Error ? error.message : String(error)}` };
|
|
613
674
|
}
|
|
614
|
-
const detail = await client.get(`/api/skills/${record.skillId}`);
|
|
615
675
|
const latestVersion = normalizeText(detail.latestVersion);
|
|
616
676
|
if (!latestVersion) {
|
|
617
|
-
|
|
618
|
-
continue;
|
|
677
|
+
return { record, reason: 'no-version', message: `Skipping ${record.skillName}: no latest version is published.` };
|
|
619
678
|
}
|
|
620
679
|
if (latestVersion === normalizeText(record.skillVersion)) {
|
|
621
|
-
|
|
622
|
-
|
|
680
|
+
return { record, reason: 'current', message: `Skipping ${record.skillName}: ${latestVersion} is already installed in ${formatInstallTargetRoot(record.targetRoot)}.` };
|
|
681
|
+
}
|
|
682
|
+
return { record, detail, latestVersion };
|
|
683
|
+
});
|
|
684
|
+
const candidates = [];
|
|
685
|
+
for (const result of results) {
|
|
686
|
+
if ('reason' in result) {
|
|
687
|
+
options?.onSkipped?.(result);
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
candidates.push(result);
|
|
623
691
|
}
|
|
624
|
-
candidates.push({
|
|
625
|
-
record,
|
|
626
|
-
detail,
|
|
627
|
-
latestVersion,
|
|
628
|
-
});
|
|
629
692
|
}
|
|
630
693
|
return candidates;
|
|
631
694
|
}
|
|
695
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
696
|
+
const results = new Array(items.length);
|
|
697
|
+
let nextIndex = 0;
|
|
698
|
+
async function run() {
|
|
699
|
+
while (nextIndex < items.length) {
|
|
700
|
+
const index = nextIndex;
|
|
701
|
+
nextIndex += 1;
|
|
702
|
+
results[index] = await worker(items[index]);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, run));
|
|
706
|
+
return results;
|
|
707
|
+
}
|
|
632
708
|
function formatUpdateSelectionDescription(options) {
|
|
633
709
|
const parts = [];
|
|
634
710
|
if (options.skillRef) {
|
|
@@ -749,7 +825,7 @@ async function promptForAgents(scope) {
|
|
|
749
825
|
})),
|
|
750
826
|
],
|
|
751
827
|
initialValues,
|
|
752
|
-
required:
|
|
828
|
+
required: true,
|
|
753
829
|
});
|
|
754
830
|
if (isCancel(selected)) {
|
|
755
831
|
cancel('Cancelled.');
|
|
@@ -885,31 +961,81 @@ async function publishSkillFromDirectory(client, localSkill, input) {
|
|
|
885
961
|
}
|
|
886
962
|
async function writeSkillVersionToDirectory(client, input, installProgress) {
|
|
887
963
|
installProgress?.onTargetStart(input.targetRoot);
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
964
|
+
const previous = await findInstalledSkillRecord(client.baseUrl, input.targetRoot);
|
|
965
|
+
const expectedSizes = new Map(input.files.map((file) => [file.nameWithPath, file.length]));
|
|
966
|
+
const manifest = await installFilesAtomically({
|
|
967
|
+
targetRoot: input.targetRoot,
|
|
968
|
+
files: input.files.map((file) => ({ relativePath: file.nameWithPath })),
|
|
969
|
+
previousManifest: previous?.files,
|
|
970
|
+
async download(relativePath, destination) {
|
|
971
|
+
installProgress?.onFileStart(input.targetRoot, relativePath, expectedSizes.get(relativePath) ?? 0);
|
|
972
|
+
const response = await client.rawGet(`/api/skills/${input.skillId}/versions/${input.version}/files/${relativePath}`);
|
|
973
|
+
const hash = await downloadResponseToFile(response, destination, (bytes) => {
|
|
974
|
+
installProgress?.onBytesTransferred(bytes);
|
|
975
|
+
});
|
|
976
|
+
installProgress?.onFileComplete();
|
|
977
|
+
return hash;
|
|
978
|
+
},
|
|
979
|
+
});
|
|
898
980
|
await upsertInstalledSkillRecord(client.baseUrl, input.targetRoot, {
|
|
899
981
|
skillId: input.skillId,
|
|
900
982
|
skillName: input.skillName,
|
|
901
983
|
skillDescription: input.skillDescription,
|
|
902
984
|
skillVersion: input.version,
|
|
903
|
-
});
|
|
985
|
+
}, manifest);
|
|
904
986
|
installProgress?.onTargetComplete();
|
|
905
987
|
}
|
|
988
|
+
async function findInstalledSkillRecord(baseUrl, targetRoot) {
|
|
989
|
+
const resolvedTargetRoot = resolve(targetRoot);
|
|
990
|
+
const records = (await readInstalledSkillRecords()).filter((record) => record.targetRoot === resolvedTargetRoot);
|
|
991
|
+
return records.find((record) => record.baseUrl === normalizeBaseUrl(baseUrl)) ?? records[0] ?? null;
|
|
992
|
+
}
|
|
993
|
+
async function ensureTargetsWritable(baseUrl, targetRoots, options) {
|
|
994
|
+
if (options.force) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const conflicts = [];
|
|
998
|
+
for (const targetRoot of targetRoots) {
|
|
999
|
+
// Directories installed before installed.json existed still carry the legacy metadata file.
|
|
1000
|
+
const record = await findInstalledSkillRecord(baseUrl, targetRoot)
|
|
1001
|
+
?? (existsSync(join(targetRoot, SKILL_METADATA_FILE_NAME)) ? {} : null);
|
|
1002
|
+
const state = await inspectInstallTarget(targetRoot, record);
|
|
1003
|
+
if (state.state === 'modified') {
|
|
1004
|
+
conflicts.push(` ${formatInstallTargetRoot(targetRoot)}: local changes in ${formatChangedPaths(state.paths)}`);
|
|
1005
|
+
}
|
|
1006
|
+
else if (state.state === 'untracked') {
|
|
1007
|
+
conflicts.push(` ${formatInstallTargetRoot(targetRoot)}: contains files not installed by Skillshare`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (conflicts.length === 0) {
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (options.interactive) {
|
|
1014
|
+
log.warn(`These install locations would be overwritten:\n${conflicts.join('\n')}`);
|
|
1015
|
+
if (await promptConfirm('Overwrite them?', false)) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
cancel('Install cancelled. Nothing was changed.');
|
|
1019
|
+
process.exit(0);
|
|
1020
|
+
}
|
|
1021
|
+
throw new Error(`Refusing to overwrite:\n${conflicts.join('\n')}\nRe-run with --force to overwrite.`);
|
|
1022
|
+
}
|
|
1023
|
+
function formatChangedPaths(paths) {
|
|
1024
|
+
const shown = paths.slice(0, 3).join(', ');
|
|
1025
|
+
return paths.length > 3 ? `${shown} and ${paths.length - 3} more` : shown;
|
|
1026
|
+
}
|
|
1027
|
+
function formatCliPath(path) {
|
|
1028
|
+
const display = formatInstallTargetRoot(path);
|
|
1029
|
+
return /^[A-Za-z0-9._/~-]+$/.test(display) ? display : JSON.stringify(display);
|
|
1030
|
+
}
|
|
906
1031
|
async function downloadResponseToFile(response, destination, onBytesTransferred) {
|
|
1032
|
+
const hash = createHash('sha256');
|
|
907
1033
|
const body = response.body;
|
|
908
1034
|
if (!body) {
|
|
909
1035
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
910
1036
|
await writeFile(destination, buffer);
|
|
911
1037
|
onBytesTransferred?.(buffer.length);
|
|
912
|
-
return;
|
|
1038
|
+
return hash.update(buffer).digest('hex');
|
|
913
1039
|
}
|
|
914
1040
|
const fileHandle = await open(destination, 'w');
|
|
915
1041
|
let failed = true;
|
|
@@ -925,6 +1051,7 @@ async function downloadResponseToFile(response, destination, onBytesTransferred)
|
|
|
925
1051
|
continue;
|
|
926
1052
|
}
|
|
927
1053
|
await fileHandle.writeFile(value);
|
|
1054
|
+
hash.update(value);
|
|
928
1055
|
onBytesTransferred?.(value.length);
|
|
929
1056
|
}
|
|
930
1057
|
}
|
|
@@ -934,6 +1061,7 @@ async function downloadResponseToFile(response, destination, onBytesTransferred)
|
|
|
934
1061
|
await rm(destination, { force: true });
|
|
935
1062
|
}
|
|
936
1063
|
}
|
|
1064
|
+
return hash.digest('hex');
|
|
937
1065
|
}
|
|
938
1066
|
function createInstallProgressReporter(input) {
|
|
939
1067
|
const installStatus = spinner({ indicator: 'timer' });
|
|
@@ -1349,9 +1477,15 @@ async function login(baseUrl) {
|
|
|
1349
1477
|
const loginUrl = `${baseUrl}/api/cli/login?redirectUri=${encodeURIComponent(callback.redirectUri)}` +
|
|
1350
1478
|
`&codeChallenge=${encodeURIComponent(codeChallenge)}` +
|
|
1351
1479
|
`&state=${encodeURIComponent(state)}`;
|
|
1352
|
-
|
|
1353
|
-
console.log(`
|
|
1354
|
-
|
|
1480
|
+
console.log(`Opening the browser to sign in to ${baseUrl}.`);
|
|
1481
|
+
console.log(`If it does not open, visit this URL on this computer:\n ${loginUrl}`);
|
|
1482
|
+
try {
|
|
1483
|
+
await openBrowser(loginUrl);
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
console.log('Could not open a browser automatically. Use the URL above.');
|
|
1487
|
+
}
|
|
1488
|
+
const authCode = await callback.wait(state, LOGIN_TIMEOUT_MS);
|
|
1355
1489
|
const response = await fetchJson(`${baseUrl}/api/cli/token`, {
|
|
1356
1490
|
method: 'POST',
|
|
1357
1491
|
headers: {
|
|
@@ -1373,6 +1507,7 @@ async function login(baseUrl) {
|
|
|
1373
1507
|
user: response.user,
|
|
1374
1508
|
});
|
|
1375
1509
|
console.log(`Logged in as ${response.user.displayName} (${baseUrl})`);
|
|
1510
|
+
return response.user;
|
|
1376
1511
|
}
|
|
1377
1512
|
const GCM_MINIMUM_VERSION = [2, 0, 931];
|
|
1378
1513
|
async function configureGitCredentialManager(baseUrl) {
|
|
@@ -1434,36 +1569,8 @@ function compareVersions(left, right) {
|
|
|
1434
1569
|
function formatVersion(version) {
|
|
1435
1570
|
return version.join('.');
|
|
1436
1571
|
}
|
|
1437
|
-
async function parseOptions(args) {
|
|
1438
|
-
const
|
|
1439
|
-
const flags = {};
|
|
1440
|
-
const positionals = [];
|
|
1441
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1442
|
-
const current = args[index];
|
|
1443
|
-
if (!current) {
|
|
1444
|
-
continue;
|
|
1445
|
-
}
|
|
1446
|
-
if (current.startsWith('--')) {
|
|
1447
|
-
const key = current.slice(2);
|
|
1448
|
-
const value = args[index + 1];
|
|
1449
|
-
if (key === 'headless' && value !== 'true' && value !== 'false') {
|
|
1450
|
-
flags[key] = 'true';
|
|
1451
|
-
continue;
|
|
1452
|
-
}
|
|
1453
|
-
if (!value || value.startsWith('--')) {
|
|
1454
|
-
if (booleanFlags.has(key)) {
|
|
1455
|
-
flags[key] = 'true';
|
|
1456
|
-
continue;
|
|
1457
|
-
}
|
|
1458
|
-
throw new Error(`Missing value for ${current}`);
|
|
1459
|
-
}
|
|
1460
|
-
flags[key] = value;
|
|
1461
|
-
index += 1;
|
|
1462
|
-
}
|
|
1463
|
-
else {
|
|
1464
|
-
positionals.push(current);
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1572
|
+
async function parseOptions(args, commandName) {
|
|
1573
|
+
const { flags, positionals } = parseCliArgs(args, commandName);
|
|
1467
1574
|
const tokenFromFlag = normalizeText(flags.token);
|
|
1468
1575
|
const tokenFromEnvironment = normalizeText(process.env.SKILLSHARE_TOKEN);
|
|
1469
1576
|
return {
|
|
@@ -1500,7 +1607,7 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
1500
1607
|
if (new Date(currentSession.refreshTokenExpiresAt).getTime() <= Date.now()) {
|
|
1501
1608
|
throw new Error(`Stored session for ${resolvedBaseUrl} is no longer valid. Run "skillshare login${resolvedBaseUrl === LOCAL_URL ? ' local' : ''}" again.`);
|
|
1502
1609
|
}
|
|
1503
|
-
const response = await
|
|
1610
|
+
const response = await fetchFromServer(`${resolvedBaseUrl}/api/cli/refresh`, {
|
|
1504
1611
|
method: 'POST',
|
|
1505
1612
|
headers: {
|
|
1506
1613
|
'Content-Type': 'application/json',
|
|
@@ -1537,7 +1644,7 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
1537
1644
|
return refreshedSession.accessToken;
|
|
1538
1645
|
}
|
|
1539
1646
|
async function sendAuthorized(path, init, retryUnauthorized = true) {
|
|
1540
|
-
const response = await
|
|
1647
|
+
const response = await fetchFromServer(`${resolvedBaseUrl}${path}`, {
|
|
1541
1648
|
...init,
|
|
1542
1649
|
headers: await buildHeaders(() => getAccessToken(), init?.headers),
|
|
1543
1650
|
});
|
|
@@ -1545,7 +1652,7 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
1545
1652
|
return response;
|
|
1546
1653
|
}
|
|
1547
1654
|
const refreshedToken = await getAccessToken({ forceRefresh: true });
|
|
1548
|
-
return
|
|
1655
|
+
return fetchFromServer(`${resolvedBaseUrl}${path}`, {
|
|
1549
1656
|
...init,
|
|
1550
1657
|
headers: await buildHeaders(() => Promise.resolve(refreshedToken), init?.headers),
|
|
1551
1658
|
});
|
|
@@ -1569,7 +1676,7 @@ async function createClient(baseUrl, explicitToken) {
|
|
|
1569
1676
|
async rawGet(path) {
|
|
1570
1677
|
const response = await sendAuthorized(path);
|
|
1571
1678
|
if (!response.ok) {
|
|
1572
|
-
throw new
|
|
1679
|
+
throw new HttpError(response.status, await formatErrorResponse(response));
|
|
1573
1680
|
}
|
|
1574
1681
|
return response;
|
|
1575
1682
|
},
|
|
@@ -1624,9 +1731,34 @@ async function buildHeaders(getAccessToken, initialHeaders) {
|
|
|
1624
1731
|
headers.set('Authorization', `Bearer ${await getAccessToken()}`);
|
|
1625
1732
|
return headers;
|
|
1626
1733
|
}
|
|
1734
|
+
class HttpError extends Error {
|
|
1735
|
+
status;
|
|
1736
|
+
constructor(status, message) {
|
|
1737
|
+
super(message);
|
|
1738
|
+
this.status = status;
|
|
1739
|
+
this.name = 'HttpError';
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
class NetworkError extends Error {
|
|
1743
|
+
constructor(origin, cause) {
|
|
1744
|
+
super(`Cannot reach ${origin}. Check your network or VPN connection and try again.`, { cause });
|
|
1745
|
+
this.name = 'NetworkError';
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
async function fetchFromServer(input, init) {
|
|
1749
|
+
try {
|
|
1750
|
+
return await fetch(input, init);
|
|
1751
|
+
}
|
|
1752
|
+
catch (error) {
|
|
1753
|
+
if (init?.signal?.aborted) {
|
|
1754
|
+
throw error;
|
|
1755
|
+
}
|
|
1756
|
+
throw new NetworkError(new URL(input).origin, error);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1627
1759
|
async function parseJson(response) {
|
|
1628
1760
|
if (!response.ok) {
|
|
1629
|
-
throw new
|
|
1761
|
+
throw new HttpError(response.status, await formatErrorResponse(response));
|
|
1630
1762
|
}
|
|
1631
1763
|
return response.json();
|
|
1632
1764
|
}
|
|
@@ -1661,7 +1793,7 @@ async function formatErrorResponse(response) {
|
|
|
1661
1793
|
: `${fallbackMessage} ${body}`;
|
|
1662
1794
|
}
|
|
1663
1795
|
async function fetchJson(input, init) {
|
|
1664
|
-
const response = await
|
|
1796
|
+
const response = await fetchFromServer(input, init);
|
|
1665
1797
|
return parseJson(response);
|
|
1666
1798
|
}
|
|
1667
1799
|
async function loadSession(baseUrl) {
|
|
@@ -1767,15 +1899,22 @@ async function waitForLoginCallback() {
|
|
|
1767
1899
|
}
|
|
1768
1900
|
return {
|
|
1769
1901
|
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
1770
|
-
async wait(expectedState) {
|
|
1902
|
+
async wait(expectedState, timeoutMs) {
|
|
1903
|
+
let timer;
|
|
1904
|
+
const timeout = new Promise((_, reject) => {
|
|
1905
|
+
timer = setTimeout(() => {
|
|
1906
|
+
reject(new Error(`Sign-in did not complete within ${Math.round(timeoutMs / 60_000)} minutes. Run "skillshare login" again.`));
|
|
1907
|
+
}, timeoutMs);
|
|
1908
|
+
});
|
|
1771
1909
|
try {
|
|
1772
|
-
const payload = JSON.parse(await statePromise);
|
|
1910
|
+
const payload = JSON.parse(await Promise.race([statePromise, timeout]));
|
|
1773
1911
|
if (payload.state !== expectedState) {
|
|
1774
1912
|
throw new Error('Login state mismatch.');
|
|
1775
1913
|
}
|
|
1776
1914
|
return payload.code;
|
|
1777
1915
|
}
|
|
1778
1916
|
finally {
|
|
1917
|
+
clearTimeout(timer);
|
|
1779
1918
|
server.close();
|
|
1780
1919
|
}
|
|
1781
1920
|
},
|
|
@@ -1832,7 +1971,7 @@ async function upsertUploadedSkillMetadata(baseUrl, directory, metadata) {
|
|
|
1832
1971
|
await writeStateFile(UPLOADED_STATE_PATH, records);
|
|
1833
1972
|
await removeLegacyLocalSkillMetadata(resolvedDirectory);
|
|
1834
1973
|
}
|
|
1835
|
-
async function upsertInstalledSkillRecord(baseUrl, targetRoot, metadata) {
|
|
1974
|
+
async function upsertInstalledSkillRecord(baseUrl, targetRoot, metadata, files) {
|
|
1836
1975
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
1837
1976
|
const resolvedTargetRoot = resolve(targetRoot);
|
|
1838
1977
|
const records = (await readInstalledSkillRecords())
|
|
@@ -1842,6 +1981,7 @@ async function upsertInstalledSkillRecord(baseUrl, targetRoot, metadata) {
|
|
|
1842
1981
|
targetRoot: resolvedTargetRoot,
|
|
1843
1982
|
baseUrl: normalizedBaseUrl,
|
|
1844
1983
|
updatedAt: new Date().toISOString(),
|
|
1984
|
+
...(files ? { files } : {}),
|
|
1845
1985
|
});
|
|
1846
1986
|
await writeStateFile(INSTALLED_STATE_PATH, records);
|
|
1847
1987
|
await removeLegacyLocalSkillMetadata(resolvedTargetRoot);
|
|
@@ -1883,9 +2023,17 @@ async function readInstalledSkillRecords() {
|
|
|
1883
2023
|
skillName: typeof entry.skillName === 'string' ? entry.skillName : '',
|
|
1884
2024
|
skillDescription: typeof entry.skillDescription === 'string' ? entry.skillDescription : '',
|
|
1885
2025
|
skillVersion: typeof entry.skillVersion === 'string' ? entry.skillVersion : '',
|
|
2026
|
+
files: readFileManifest(entry.files),
|
|
1886
2027
|
}].filter((record) => record.targetRoot.length > 0);
|
|
1887
2028
|
});
|
|
1888
2029
|
}
|
|
2030
|
+
function readFileManifest(value) {
|
|
2031
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2032
|
+
return undefined;
|
|
2033
|
+
}
|
|
2034
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === 'string');
|
|
2035
|
+
return Object.fromEntries(entries);
|
|
2036
|
+
}
|
|
1889
2037
|
async function readStateFile(path) {
|
|
1890
2038
|
try {
|
|
1891
2039
|
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { mkdir, readdir, rename, rm, rmdir, stat } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
export function resolveInsideRoot(root, relativePath) {
|
|
6
|
+
const resolvedRoot = resolve(root);
|
|
7
|
+
const destination = resolve(resolvedRoot, relativePath);
|
|
8
|
+
if (destination === resolvedRoot || !destination.startsWith(resolvedRoot + sep)) {
|
|
9
|
+
throw new Error(`Refusing to write '${relativePath}' outside ${resolvedRoot}.`);
|
|
10
|
+
}
|
|
11
|
+
return destination;
|
|
12
|
+
}
|
|
13
|
+
export async function hashFile(path) {
|
|
14
|
+
const hash = createHash('sha256');
|
|
15
|
+
for await (const chunk of createReadStream(path)) {
|
|
16
|
+
hash.update(chunk);
|
|
17
|
+
}
|
|
18
|
+
return hash.digest('hex');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Compares a target directory with the manifest recorded at install time.
|
|
22
|
+
* A record without a manifest predates change tracking and cannot be verified.
|
|
23
|
+
*/
|
|
24
|
+
export async function inspectInstallTarget(targetRoot, record) {
|
|
25
|
+
if (!await isNonEmptyDirectory(targetRoot)) {
|
|
26
|
+
return { state: 'missing' };
|
|
27
|
+
}
|
|
28
|
+
if (!record) {
|
|
29
|
+
return { state: 'untracked' };
|
|
30
|
+
}
|
|
31
|
+
if (!record.files) {
|
|
32
|
+
return { state: 'legacy' };
|
|
33
|
+
}
|
|
34
|
+
const modified = [];
|
|
35
|
+
for (const [relativePath, expectedHash] of Object.entries(record.files)) {
|
|
36
|
+
const path = resolveInsideRoot(targetRoot, relativePath);
|
|
37
|
+
try {
|
|
38
|
+
if (await hashFile(path) !== expectedHash)
|
|
39
|
+
modified.push(relativePath);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error?.code !== 'ENOENT')
|
|
43
|
+
throw error;
|
|
44
|
+
modified.push(relativePath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return modified.length > 0 ? { state: 'modified', paths: modified } : { state: 'clean' };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Downloads every file into a sibling staging directory first, so a failed transfer
|
|
51
|
+
* leaves the existing install untouched. Files from the previous manifest that the
|
|
52
|
+
* new version no longer contains are removed afterwards.
|
|
53
|
+
*/
|
|
54
|
+
export async function installFilesAtomically(input) {
|
|
55
|
+
const targetRoot = resolve(input.targetRoot);
|
|
56
|
+
const stagingRoot = join(dirname(targetRoot), `.${basename(targetRoot)}.skillshare-${randomBytes(4).toString('hex')}`);
|
|
57
|
+
const manifest = {};
|
|
58
|
+
await mkdir(stagingRoot, { recursive: true });
|
|
59
|
+
try {
|
|
60
|
+
for (const file of input.files) {
|
|
61
|
+
const stagedPath = resolveInsideRoot(stagingRoot, file.relativePath);
|
|
62
|
+
resolveInsideRoot(targetRoot, file.relativePath);
|
|
63
|
+
await mkdir(dirname(stagedPath), { recursive: true });
|
|
64
|
+
manifest[normalizeRelativePath(file.relativePath)] = await input.download(file.relativePath, stagedPath);
|
|
65
|
+
}
|
|
66
|
+
await mkdir(targetRoot, { recursive: true });
|
|
67
|
+
for (const relativePath of Object.keys(manifest)) {
|
|
68
|
+
const destination = resolveInsideRoot(targetRoot, relativePath);
|
|
69
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
70
|
+
await rename(resolveInsideRoot(stagingRoot, relativePath), destination);
|
|
71
|
+
}
|
|
72
|
+
for (const relativePath of Object.keys(input.previousManifest ?? {})) {
|
|
73
|
+
if (relativePath in manifest)
|
|
74
|
+
continue;
|
|
75
|
+
const stalePath = resolveInsideRoot(targetRoot, relativePath);
|
|
76
|
+
await rm(stalePath, { force: true });
|
|
77
|
+
await removeEmptyParents(dirname(stalePath), targetRoot);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
return manifest;
|
|
84
|
+
}
|
|
85
|
+
function normalizeRelativePath(path) {
|
|
86
|
+
return relative('.', join('.', path)).replaceAll('\\', '/');
|
|
87
|
+
}
|
|
88
|
+
async function isNonEmptyDirectory(path) {
|
|
89
|
+
try {
|
|
90
|
+
if (!(await stat(path)).isDirectory())
|
|
91
|
+
return true;
|
|
92
|
+
return (await readdir(path)).length > 0;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error?.code === 'ENOENT')
|
|
96
|
+
return false;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function removeEmptyParents(directory, root) {
|
|
101
|
+
let current = resolve(directory);
|
|
102
|
+
while (current !== root && current.startsWith(root + sep)) {
|
|
103
|
+
try {
|
|
104
|
+
await rmdir(current);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
current = dirname(current);
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/teams-auth.js
CHANGED
|
@@ -16,7 +16,7 @@ export function decodeTeamsJwt(value) {
|
|
|
16
16
|
/** Select only credentials belonging to the Skillshare account. No full browser state leaves this machine. */
|
|
17
17
|
export function extractTeamsSession(state, expectedObjectId, now = Date.now(), allowExpired = false) {
|
|
18
18
|
if (!uuid.test(expectedObjectId))
|
|
19
|
-
throw new Error('
|
|
19
|
+
throw new Error('The Microsoft account object id is missing. Sign in to Skillshare in the browser, then run skillshare teams login again.');
|
|
20
20
|
const stores = state.origins.filter(origin => origins.has(origin.origin));
|
|
21
21
|
const entries = stores.flatMap(origin => origin.localStorage.flatMap(item => {
|
|
22
22
|
try {
|
package/dist/teams.js
CHANGED
|
@@ -7,6 +7,17 @@ import { pipeline } from 'node:stream/promises';
|
|
|
7
7
|
import { captureTeamsLogin } from './teams-auth.js';
|
|
8
8
|
import { watchTeamsConnection } from './teams-watch.js';
|
|
9
9
|
const root = '/api/mcps/providers/teams';
|
|
10
|
+
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
|
+
* Skillshare identifies users by the pairwise subject; Teams credentials carry the Entra
|
|
13
|
+
* object id, which the server records at web sign-in. Older servers only return a GUID id.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveTeamsObjectId(account) {
|
|
16
|
+
if (account.entraObjectId && guid.test(account.entraObjectId))
|
|
17
|
+
return account.entraObjectId;
|
|
18
|
+
return guid.test(account.id) ? account.id : null;
|
|
19
|
+
}
|
|
20
|
+
export const UNLINKED_ACCOUNT_MESSAGE = 'Skillshare does not know your Microsoft account yet. Run "skillshare login" (or open Skillshare in the browser and sign in), then run "skillshare teams login" again.';
|
|
10
21
|
async function expectOk(response) {
|
|
11
22
|
if (response.ok)
|
|
12
23
|
return;
|
|
@@ -23,11 +34,15 @@ export async function runTeamsCommand(client, args, flags, appDirectory) {
|
|
|
23
34
|
const action = args[0];
|
|
24
35
|
if (action === 'login' || action === 'watch') {
|
|
25
36
|
const user = await client.get('/api/auth/me');
|
|
37
|
+
const resolvedObjectId = resolveTeamsObjectId(user);
|
|
38
|
+
if (!resolvedObjectId)
|
|
39
|
+
throw new Error(UNLINKED_ACCOUNT_MESSAGE);
|
|
40
|
+
const objectId = resolvedObjectId;
|
|
26
41
|
const key = createHash('sha256').update(client.baseUrl + '\0' + user.id).digest('hex').slice(0, 24);
|
|
27
42
|
const profileDirectory = flags['browser-profile'] ? resolve(flags['browser-profile']) : join(appDirectory, 'teams', key, 'browser-profile');
|
|
28
43
|
const abort = new AbortController();
|
|
29
44
|
async function connect(connectionId) {
|
|
30
|
-
const session = await captureTeamsLogin({ profileDirectory, objectId
|
|
45
|
+
const session = await captureTeamsLogin({ profileDirectory, objectId, browser: flags.browser,
|
|
31
46
|
headless: action === 'watch' ? flags.headless !== 'false' : flags.headless === 'true', recover: action === 'watch',
|
|
32
47
|
signal: abort.signal, timeoutMs: action === 'watch' ? 90_000 : undefined, onProgress: console.log });
|
|
33
48
|
const headers = { 'Content-Type': 'application/json' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quadient/skillshare",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
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 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 pages.test.ts pages-dev.test.ts teams-auth.test.ts teams-watch.test.ts teams-browser.test.ts"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
22
22
|
"access": "public"
|