skills-layer 0.1.0 → 0.1.2
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/package.json +6 -4
- package/public/skills-layer-public.mjs +239 -71
- package/tests/public-cli-package.test.mjs +169 -0
- package/scripts/postinstall.mjs +0 -92
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { mkdtemp, readFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const packageUrl = new URL("../package.json", import.meta.url);
|
|
10
|
+
const runtimeUrl = new URL("../public/skills-layer-public.mjs", import.meta.url);
|
|
11
|
+
const readmeUrl = new URL("../README.md", import.meta.url);
|
|
12
|
+
const licenseUrl = new URL("../LICENSE", import.meta.url);
|
|
13
|
+
const wrapperUrl = new URL("../scripts/skills-layer.mjs", import.meta.url);
|
|
14
|
+
const binPath = fileURLToPath(new URL("../bin/skills-layer.js", import.meta.url));
|
|
15
|
+
|
|
16
|
+
async function loadRuntime() {
|
|
17
|
+
return await import(runtimeUrl.href);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function makeHome() {
|
|
21
|
+
return await mkdtemp(join(tmpdir(), "skills-layer-cli-package-home-"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function packageEnv(home) {
|
|
25
|
+
const env = {
|
|
26
|
+
...process.env,
|
|
27
|
+
HOME: home,
|
|
28
|
+
SKILLS_LAYER_NO_BANNER: "1"
|
|
29
|
+
};
|
|
30
|
+
delete env.SKILLS_LAYER_API_KEY;
|
|
31
|
+
delete env.SKILLS_LAYER_BASE_URL;
|
|
32
|
+
delete env.SKILLS_LAYER_MCP_TOKEN;
|
|
33
|
+
return env;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runPublic(args, options = {}) {
|
|
37
|
+
const runtime = await loadRuntime();
|
|
38
|
+
return await runtime.executePublicCli(args, {
|
|
39
|
+
env: packageEnv(options.home ?? await makeHome()),
|
|
40
|
+
cwd: options.cwd,
|
|
41
|
+
stdoutIsTty: false,
|
|
42
|
+
fetch: options.fetch ?? (async () => { throw new Error("Unexpected network call from package contract test."); })
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function runBin(args) {
|
|
47
|
+
const home = await makeHome();
|
|
48
|
+
return await new Promise((resolve) => {
|
|
49
|
+
execFile(process.execPath, [binPath, ...args], { env: packageEnv(home) }, (error, stdout, stderr) => {
|
|
50
|
+
resolve({
|
|
51
|
+
exitCode: typeof error?.code === "number" ? error.code : 0,
|
|
52
|
+
stdout,
|
|
53
|
+
stderr
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseJsonOutput(result) {
|
|
60
|
+
assert.equal(result.stderr, "");
|
|
61
|
+
return JSON.parse(result.stdout);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseJsonError(result) {
|
|
65
|
+
assert.equal(result.stdout, "");
|
|
66
|
+
return JSON.parse(result.stderr);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test("public package metadata keeps the npm tree narrow and non-lifecycle", async () => {
|
|
70
|
+
const packageJson = JSON.parse(await readFile(packageUrl, "utf8"));
|
|
71
|
+
assert.equal(packageJson.name, "skills-layer");
|
|
72
|
+
assert.equal(packageJson.private, false);
|
|
73
|
+
assert.equal(packageJson.type, "module");
|
|
74
|
+
assert.equal(packageJson.bin["skills-layer"], "bin/skills-layer.js");
|
|
75
|
+
assert.equal(packageJson.dependencies, undefined);
|
|
76
|
+
assert.equal(packageJson.devDependencies, undefined);
|
|
77
|
+
assert.equal(packageJson.repository, undefined);
|
|
78
|
+
assert.equal(packageJson.bugs, undefined);
|
|
79
|
+
assert.deepEqual(packageJson.files, [
|
|
80
|
+
"bin/skills-layer.js",
|
|
81
|
+
"public/skills-layer-public.mjs",
|
|
82
|
+
"scripts/skills-layer.mjs",
|
|
83
|
+
"docs/assets/*.svg",
|
|
84
|
+
"tests/public-cli-package.test.mjs"
|
|
85
|
+
]);
|
|
86
|
+
assert.deepEqual(Object.keys(packageJson.scripts).sort(), ["test", "test:coverage", "typecheck"]);
|
|
87
|
+
for (const lifecycleScript of ["preinstall", "install", "postinstall", "prepare", "prepack", "postpack", "prepublishOnly"]) {
|
|
88
|
+
assert.equal(packageJson.scripts[lifecycleScript], undefined);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("public package text stays limited to public CLI surface", async () => {
|
|
93
|
+
const [packageText, readme, license, runtime, wrapper] = await Promise.all([
|
|
94
|
+
readFile(packageUrl, "utf8"),
|
|
95
|
+
readFile(readmeUrl, "utf8"),
|
|
96
|
+
readFile(licenseUrl, "utf8"),
|
|
97
|
+
readFile(runtimeUrl, "utf8"),
|
|
98
|
+
readFile(wrapperUrl, "utf8")
|
|
99
|
+
]);
|
|
100
|
+
const combined = `${packageText}\n${readme}\n${license}\n${runtime}\n${wrapper}`;
|
|
101
|
+
assert.match(combined, /Skills Layer/u);
|
|
102
|
+
assert.match(combined, /skills-layer/u);
|
|
103
|
+
for (const forbidden of [
|
|
104
|
+
new RegExp(["", "api", "v1", "admin"].join("/"), "u"),
|
|
105
|
+
new RegExp(["x", "skills", "layer", "admin", "key"].join("-"), "u"),
|
|
106
|
+
new RegExp(["SKILLS", "LAYER", "ADMIN", "API", "KEY"].join("_"), "u"),
|
|
107
|
+
new RegExp(["--", "admin", "api", "key"].join("-"), "u"),
|
|
108
|
+
new RegExp(["github.com", "openai", "openskills"].join("/"), "u")
|
|
109
|
+
]) {
|
|
110
|
+
assert.doesNotMatch(combined, forbidden);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("public bin wrapper and runtime expose deterministic package smoke commands", async () => {
|
|
115
|
+
const runtime = await loadRuntime();
|
|
116
|
+
const binVersion = await runBin(["--version"]);
|
|
117
|
+
assert.equal(binVersion.exitCode, 0);
|
|
118
|
+
assert.equal(binVersion.stderr, "");
|
|
119
|
+
assert.match(binVersion.stdout.trim(), /^skills-layer 0\.1\.2$/u);
|
|
120
|
+
|
|
121
|
+
const version = await runtime.executePublicCli(["--version"]);
|
|
122
|
+
assert.equal(version.exitCode, 0);
|
|
123
|
+
assert.match(version.stdout.trim(), /^skills-layer 0\.1\.2$/u);
|
|
124
|
+
|
|
125
|
+
const jsonVersion = await runPublic(["version", "--json"]);
|
|
126
|
+
assert.equal(jsonVersion.exitCode, 0);
|
|
127
|
+
assert.equal(parseJsonOutput(jsonVersion).data.version, "0.1.2");
|
|
128
|
+
|
|
129
|
+
const loginHelp = await runtime.executePublicCli(["login", "--help"]);
|
|
130
|
+
assert.equal(loginHelp.exitCode, 0);
|
|
131
|
+
assert.match(loginHelp.stdout, /Skills Layer login help/u);
|
|
132
|
+
assert.match(loginHelp.stdout, /skills-layer mcp setup/u);
|
|
133
|
+
|
|
134
|
+
const fullHelp = await runtime.executePublicCli(["help", "full"]);
|
|
135
|
+
assert.equal(fullHelp.exitCode, 0);
|
|
136
|
+
assert.match(fullHelp.stdout, /Start here:/u);
|
|
137
|
+
assert.match(fullHelp.stdout, /skills-layer help full/u);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("public runtime persists backend preference without network access", async () => {
|
|
141
|
+
const home = await makeHome();
|
|
142
|
+
const update = await runPublic(["backend", "--base-url", "https://staging.findfigg.co.uk", "--json"], { home });
|
|
143
|
+
assert.equal(update.exitCode, 0);
|
|
144
|
+
assert.equal(parseJsonOutput(update).data.selectedBaseUrl, "https://staging.findfigg.co.uk");
|
|
145
|
+
|
|
146
|
+
const preference = JSON.parse(await readFile(join(home, ".skills-layer", "backend-preference.json"), "utf8"));
|
|
147
|
+
assert.equal(preference.preferredBaseUrl, "https://staging.findfigg.co.uk");
|
|
148
|
+
|
|
149
|
+
const status = await runPublic(["backend", "status", "--json"], { home });
|
|
150
|
+
assert.equal(status.exitCode, 0);
|
|
151
|
+
const statusPayload = parseJsonOutput(status);
|
|
152
|
+
assert.equal(statusPayload.data.selectedBaseUrl, "https://staging.findfigg.co.uk");
|
|
153
|
+
assert.equal(statusPayload.data.loggedIn, false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("public runtime fails closed for internal and login-required commands", async () => {
|
|
157
|
+
const admin = await runPublic(["admin", "status", "--json"]);
|
|
158
|
+
assert.equal(admin.exitCode, 1);
|
|
159
|
+
const adminPayload = parseJsonError(admin);
|
|
160
|
+
assert.equal(adminPayload.status, "error");
|
|
161
|
+
assert.match(adminPayload.summary, /does not include internal commands/u);
|
|
162
|
+
|
|
163
|
+
const whoami = await runPublic(["whoami", "--json"]);
|
|
164
|
+
assert.equal(whoami.exitCode, 1);
|
|
165
|
+
const whoamiPayload = parseJsonError(whoami);
|
|
166
|
+
assert.equal(whoamiPayload.status, "error");
|
|
167
|
+
assert.match(whoamiPayload.summary, /Run `skills-layer login` first/u);
|
|
168
|
+
assert.doesNotMatch(whoamiPayload.summary, /--api-key/u);
|
|
169
|
+
});
|
package/scripts/postinstall.mjs
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
|
|
6
|
-
const BANNER = ` ____ _ _ _ _ _
|
|
7
|
-
/ ___|| | _(_) | |___ | | __ _ _ _ ___ _ __
|
|
8
|
-
\\___ \\| |/ / | | / __| | | / _\` | | | |/ _ \\ '__|
|
|
9
|
-
___) | <| | | \\__ \\ | |__| (_| | |_| | __/ |
|
|
10
|
-
|____/|_|\\_\\_|_|_|___/ |_____\\__,_|\\__, |\\___|_|
|
|
11
|
-
|___/
|
|
12
|
-
Skills-Layer
|
|
13
|
-
AI Agent first platform for Skills`;
|
|
14
|
-
|
|
15
|
-
if (await shouldShowAndRecordInstallBanner(process.env)) {
|
|
16
|
-
process.stdout.write([
|
|
17
|
-
"",
|
|
18
|
-
BANNER,
|
|
19
|
-
"",
|
|
20
|
-
"Thanks for installing Skills-Layer.",
|
|
21
|
-
"Run `skills-layer setup` to connect AI tools and Security hooks, or `skills-layer --help` for commands.",
|
|
22
|
-
""
|
|
23
|
-
].join("\n"));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function shouldShowAndRecordInstallBanner(env) {
|
|
27
|
-
const mode = normalizeBannerMode(env.SKILLS_LAYER_BANNER);
|
|
28
|
-
if (mode === "off" || isTruthy(env.CI) || isTruthy(env.SKILLS_LAYER_NO_INSTALL_BANNER) || isTruthy(env.SKILLS_LAYER_NO_BANNER) || env.npm_config_loglevel === "silent") {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
if (mode === "always") {
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
if (!process.stdout.isTTY && !isTruthy(env.SKILLS_LAYER_BANNER_TTY)) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
const displayLimit = parseBannerDisplayLimit(env.SKILLS_LAYER_BANNER_LIMIT);
|
|
38
|
-
if (displayLimit <= 0) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
const statePath = resolveBannerStatePath(env);
|
|
42
|
-
if (!statePath) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
const state = await readBannerState(statePath);
|
|
47
|
-
if (state.displayCount >= displayLimit) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
await mkdir(dirname(statePath), { recursive: true });
|
|
51
|
-
await writeFile(statePath, JSON.stringify({ version: 1, displayCount: state.displayCount + 1, updatedAt: new Date().toISOString() }, null, 2), "utf8");
|
|
52
|
-
return true;
|
|
53
|
-
} catch {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function readBannerState(path) {
|
|
59
|
-
try {
|
|
60
|
-
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
61
|
-
return { displayCount: typeof parsed.displayCount === "number" ? Math.max(0, parsed.displayCount) : 0 };
|
|
62
|
-
} catch {
|
|
63
|
-
return { displayCount: 0 };
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function resolveBannerStatePath(env) {
|
|
68
|
-
const homeRoot = process.platform === "win32" ? firstNonEmpty(env.APPDATA, env.USERPROFILE, env.HOME) : firstNonEmpty(env.XDG_CONFIG_HOME, env.HOME ? join(env.HOME, ".skills-layer") : undefined);
|
|
69
|
-
if (!homeRoot) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
const stateDir = process.platform === "win32" || String(env.XDG_CONFIG_HOME ?? "").trim() ? join(homeRoot, "skills-layer") : homeRoot;
|
|
73
|
-
return join(stateDir, "banner-state.json");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function normalizeBannerMode(value) {
|
|
77
|
-
const normalized = String(value ?? "").trim().toLowerCase();
|
|
78
|
-
return normalized === "always" || normalized === "off" ? normalized : "auto";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function parseBannerDisplayLimit(value) {
|
|
82
|
-
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
83
|
-
return Number.isFinite(parsed) ? Math.max(0, parsed) : 2;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function firstNonEmpty(...values) {
|
|
87
|
-
return values.find((value) => typeof value === "string" && value.trim())?.trim() ?? null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function isTruthy(value) {
|
|
91
|
-
return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
|
|
92
|
-
}
|