@seliseblocks/cli-os 0.2.4 → 0.2.6
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/dist/commands/auth/config/save.js +12 -2
- package/dist/commands/auth/oidc-clients/save.js +17 -2
- package/dist/commands/new/web.js +40 -0
- package/dist/lib/scaffold-web/app-core.js +6 -1
- package/dist/lib/scaffold-web/blocks-lib.js +36 -2
- package/dist/skills/blocks-iam-access-control/SKILL.md +49 -49
- package/dist/skills/blocks-iam-access-control/flows/feature-gating.md +38 -38
- package/dist/skills/blocks-iam-access-control/flows/manage-roles-permissions.md +110 -110
- package/dist/skills/blocks-iam-mfa/SKILL.md +124 -124
- package/dist/skills/blocks-iam-organizations/SKILL.md +43 -43
- package/dist/skills/blocks-iam-organizations/flows/admin-mutations.md +89 -89
- package/dist/skills/blocks-iam-organizations/flows/read-and-switch.md +57 -57
- package/dist/skills/blocks-iam-sso-oidc-configuration/SKILL.md +105 -105
- package/dist/skills/blocks-mail/SKILL.md +95 -95
- package/dist/skills/blocks-notification/SKILL.md +69 -69
- package/dist/skills/blocks-notifier/SKILL.md +107 -107
- package/dist/skills/blocks-release-deployment/SKILL.md +81 -81
- package/dist/skills/blocks-secrets/SKILL.md +81 -81
- package/dist/skills/lint.mjs +168 -168
- package/package.json +1 -1
package/dist/skills/lint.mjs
CHANGED
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Consistency lint for blocks-skills/. Run: node blocks-skills/lint.mjs
|
|
3
|
-
//
|
|
4
|
-
// A skill is consumed by an AI that has ONLY the globally-installed `blocks`
|
|
5
|
-
// CLI and a project-local `@seliseblocks/client` -- never this monorepo, and
|
|
6
|
-
// `blocks skill add` pulls exactly one skill directory at a time. Checks:
|
|
7
|
-
// 1. Every skill directory has a SKILL.md with frontmatter: `name` matches the
|
|
8
|
-
// directory name, `name` <= 64 chars, `description` present, non-empty,
|
|
9
|
-
// on a single physical line (blocks-cli's own frontmatter parser --
|
|
10
|
-
// src/lib/skills.ts -- is a hand-rolled line-by-line parser with no YAML
|
|
11
|
-
// dependency; a description that wraps onto a second line silently breaks
|
|
12
|
-
// `blocks skill list`/`show`), and <= 1024 chars (hard fail) / <= 700 chars
|
|
13
|
-
// (warn -- this pack's house style target is ~400-600).
|
|
14
|
-
// 2. Relative markdown links (in SKILL.md and any flows/*.md) resolve to a
|
|
15
|
-
// real file.
|
|
16
|
-
// 3. No links leave the containing skill's own directory at all -- not into
|
|
17
|
-
// another skill's SKILL.md, not into its flows/, not to a monorepo-only
|
|
18
|
-
// file outside blocks-skills/. A skill may mention another skill BY NAME
|
|
19
|
-
// in plain text, never as a link, since the target isn't guaranteed to be
|
|
20
|
-
// present for a consumer who only pulled this one skill. Links within the
|
|
21
|
-
// same skill's own directory (SKILL.md <-> its own flows/*.md) are fine.
|
|
22
|
-
// 4. No raw API endpoint paths (e.g. `/iam/v4/...`, `/os/v4/...`) -- skills
|
|
23
|
-
// describe CLI commands and SDK methods, never the wire protocol behind
|
|
24
|
-
// them; citing a path is exactly the kind of detail that could tempt a
|
|
25
|
-
// raw fetch/curl bypass every skill already forbids.
|
|
26
|
-
// Exit 0 = clean, 1 = problems found (all listed, not just the first).
|
|
27
|
-
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
28
|
-
import { dirname, join, relative } from "node:path";
|
|
29
|
-
import { fileURLToPath } from "node:url";
|
|
30
|
-
|
|
31
|
-
const skillsDir = dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
const errors = [];
|
|
33
|
-
const warnings = [];
|
|
34
|
-
|
|
35
|
-
const DESCRIPTION_HARD_LIMIT = 1024;
|
|
36
|
-
const DESCRIPTION_WARN_LIMIT = 700;
|
|
37
|
-
const NAME_LIMIT = 64;
|
|
38
|
-
const ENDPOINT_PATTERN = /\/(iam|data|os|logic|release|localization)\/v4\/[A-Za-z0-9/{}._-]*/g;
|
|
39
|
-
|
|
40
|
-
const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
|
|
41
|
-
.filter((entry) => entry.isDirectory())
|
|
42
|
-
.map((entry) => entry.name)
|
|
43
|
-
.sort();
|
|
44
|
-
|
|
45
|
-
for (const skillName of skillDirs) {
|
|
46
|
-
const skillPath = join(skillsDir, skillName);
|
|
47
|
-
const skillMdPath = join(skillPath, "SKILL.md");
|
|
48
|
-
|
|
49
|
-
if (!existsSync(skillMdPath)) {
|
|
50
|
-
errors.push(`${skillName}/: no SKILL.md`);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
checkFrontmatter(skillName, skillMdPath);
|
|
55
|
-
checkLinksInFile(skillName, skillMdPath);
|
|
56
|
-
checkEndpointsInFile(skillMdPath);
|
|
57
|
-
|
|
58
|
-
const flowsDir = join(skillPath, "flows");
|
|
59
|
-
if (existsSync(flowsDir) && statSync(flowsDir).isDirectory()) {
|
|
60
|
-
for (const entry of readdirSync(flowsDir, { withFileTypes: true })) {
|
|
61
|
-
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
62
|
-
const flowPath = join(flowsDir, entry.name);
|
|
63
|
-
checkLinksInFile(skillName, flowPath);
|
|
64
|
-
checkEndpointsInFile(flowPath);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function checkFrontmatter(skillName, skillMdPath) {
|
|
71
|
-
const raw = readFileSync(skillMdPath, "utf8");
|
|
72
|
-
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
73
|
-
const rel = relative(skillsDir, skillMdPath);
|
|
74
|
-
|
|
75
|
-
if (!match) {
|
|
76
|
-
errors.push(`${rel}: missing frontmatter (expected a leading --- ... --- block)`);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const lines = match[1].split(/\r?\n/);
|
|
81
|
-
const nameLine = lines.find((line) => line.startsWith("name:"));
|
|
82
|
-
const descLine = lines.find((line) => line.startsWith("description:"));
|
|
83
|
-
|
|
84
|
-
if (!nameLine) {
|
|
85
|
-
errors.push(`${rel}: frontmatter has no 'name' field`);
|
|
86
|
-
} else {
|
|
87
|
-
const name = nameLine.slice("name:".length).trim();
|
|
88
|
-
if (name !== skillName) {
|
|
89
|
-
errors.push(`${rel}: name '${name}' does not match directory name '${skillName}'`);
|
|
90
|
-
}
|
|
91
|
-
if (name.length > NAME_LIMIT) {
|
|
92
|
-
errors.push(`${rel}: name is ${name.length} chars, over the ${NAME_LIMIT}-char limit`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!descLine) {
|
|
97
|
-
errors.push(`${rel}: frontmatter has no 'description' field`);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const singleLineMatch = descLine.match(/^description:\s*"(.*)"\s*$/);
|
|
102
|
-
if (!singleLineMatch) {
|
|
103
|
-
errors.push(
|
|
104
|
-
`${rel}: 'description' must be a double-quoted string on a single physical line ` +
|
|
105
|
-
`(blocks-cli's frontmatter parser reads it line-by-line -- a wrapped description silently truncates)`
|
|
106
|
-
);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const description = singleLineMatch[1];
|
|
111
|
-
if (description.length === 0) {
|
|
112
|
-
errors.push(`${rel}: 'description' is empty`);
|
|
113
|
-
} else if (description.length > DESCRIPTION_HARD_LIMIT) {
|
|
114
|
-
errors.push(`${rel}: description is ${description.length} chars, over the ${DESCRIPTION_HARD_LIMIT}-char hard limit`);
|
|
115
|
-
} else if (description.length > DESCRIPTION_WARN_LIMIT) {
|
|
116
|
-
warnings.push(`${rel}: description is ${description.length} chars, over the ${DESCRIPTION_WARN_LIMIT}-char house-style target (aim for ~400-600)`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function checkLinksInFile(skillName, filePath) {
|
|
121
|
-
const raw = readFileSync(filePath, "utf8");
|
|
122
|
-
const rel = relative(skillsDir, filePath);
|
|
123
|
-
const linkPattern = /\[[^\]]*\]\(([^)]+)\)/g;
|
|
124
|
-
const fileDir = dirname(filePath);
|
|
125
|
-
|
|
126
|
-
for (const match of raw.matchAll(linkPattern)) {
|
|
127
|
-
const target = match[1].trim();
|
|
128
|
-
if (/^[a-z]+:\/\//i.test(target) || target.startsWith("#")) continue; // external URL or in-page anchor
|
|
129
|
-
|
|
130
|
-
const [pathPart] = target.split("#");
|
|
131
|
-
if (!pathPart) continue;
|
|
132
|
-
|
|
133
|
-
const resolved = join(fileDir, pathPart);
|
|
134
|
-
if (!existsSync(resolved)) {
|
|
135
|
-
errors.push(`${rel}: broken link to '${pathPart}' (resolved: ${relative(skillsDir, resolved)})`);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const resolvedRelToSkills = relative(skillsDir, resolved).split(/[\\/]/);
|
|
140
|
-
const targetSkill = resolvedRelToSkills[0];
|
|
141
|
-
if (targetSkill !== skillName) {
|
|
142
|
-
errors.push(
|
|
143
|
-
`${rel}: link leaves this skill's own directory ('${pathPart}') -- ` +
|
|
144
|
-
`mention other skills by name in plain text instead, never a link, since ` +
|
|
145
|
-
`'blocks skill add' only copies one skill directory at a time and the target isn't guaranteed to be present`
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function checkEndpointsInFile(filePath) {
|
|
152
|
-
const raw = readFileSync(filePath, "utf8");
|
|
153
|
-
const rel = relative(skillsDir, filePath);
|
|
154
|
-
|
|
155
|
-
for (const match of raw.matchAll(ENDPOINT_PATTERN)) {
|
|
156
|
-
errors.push(`${rel}: raw API endpoint path '${match[0]}' -- describe the CLI command/SDK method instead, never the wire path`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
for (const warning of warnings) console.warn(`warning: ${warning}`);
|
|
161
|
-
if (errors.length === 0) {
|
|
162
|
-
console.log(`ok: ${skillDirs.length} skills, no problems found${warnings.length ? ` (${warnings.length} warning(s) above)` : ""}`);
|
|
163
|
-
process.exit(0);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
console.error(`${errors.length} problem(s) found:`);
|
|
167
|
-
for (const error of errors) console.error(` - ${error}`);
|
|
168
|
-
process.exit(1);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Consistency lint for blocks-skills/. Run: node blocks-skills/lint.mjs
|
|
3
|
+
//
|
|
4
|
+
// A skill is consumed by an AI that has ONLY the globally-installed `blocks`
|
|
5
|
+
// CLI and a project-local `@seliseblocks/client` -- never this monorepo, and
|
|
6
|
+
// `blocks skill add` pulls exactly one skill directory at a time. Checks:
|
|
7
|
+
// 1. Every skill directory has a SKILL.md with frontmatter: `name` matches the
|
|
8
|
+
// directory name, `name` <= 64 chars, `description` present, non-empty,
|
|
9
|
+
// on a single physical line (blocks-cli's own frontmatter parser --
|
|
10
|
+
// src/lib/skills.ts -- is a hand-rolled line-by-line parser with no YAML
|
|
11
|
+
// dependency; a description that wraps onto a second line silently breaks
|
|
12
|
+
// `blocks skill list`/`show`), and <= 1024 chars (hard fail) / <= 700 chars
|
|
13
|
+
// (warn -- this pack's house style target is ~400-600).
|
|
14
|
+
// 2. Relative markdown links (in SKILL.md and any flows/*.md) resolve to a
|
|
15
|
+
// real file.
|
|
16
|
+
// 3. No links leave the containing skill's own directory at all -- not into
|
|
17
|
+
// another skill's SKILL.md, not into its flows/, not to a monorepo-only
|
|
18
|
+
// file outside blocks-skills/. A skill may mention another skill BY NAME
|
|
19
|
+
// in plain text, never as a link, since the target isn't guaranteed to be
|
|
20
|
+
// present for a consumer who only pulled this one skill. Links within the
|
|
21
|
+
// same skill's own directory (SKILL.md <-> its own flows/*.md) are fine.
|
|
22
|
+
// 4. No raw API endpoint paths (e.g. `/iam/v4/...`, `/os/v4/...`) -- skills
|
|
23
|
+
// describe CLI commands and SDK methods, never the wire protocol behind
|
|
24
|
+
// them; citing a path is exactly the kind of detail that could tempt a
|
|
25
|
+
// raw fetch/curl bypass every skill already forbids.
|
|
26
|
+
// Exit 0 = clean, 1 = problems found (all listed, not just the first).
|
|
27
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
28
|
+
import { dirname, join, relative } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
const skillsDir = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const errors = [];
|
|
33
|
+
const warnings = [];
|
|
34
|
+
|
|
35
|
+
const DESCRIPTION_HARD_LIMIT = 1024;
|
|
36
|
+
const DESCRIPTION_WARN_LIMIT = 700;
|
|
37
|
+
const NAME_LIMIT = 64;
|
|
38
|
+
const ENDPOINT_PATTERN = /\/(iam|data|os|logic|release|localization)\/v4\/[A-Za-z0-9/{}._-]*/g;
|
|
39
|
+
|
|
40
|
+
const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
|
|
41
|
+
.filter((entry) => entry.isDirectory())
|
|
42
|
+
.map((entry) => entry.name)
|
|
43
|
+
.sort();
|
|
44
|
+
|
|
45
|
+
for (const skillName of skillDirs) {
|
|
46
|
+
const skillPath = join(skillsDir, skillName);
|
|
47
|
+
const skillMdPath = join(skillPath, "SKILL.md");
|
|
48
|
+
|
|
49
|
+
if (!existsSync(skillMdPath)) {
|
|
50
|
+
errors.push(`${skillName}/: no SKILL.md`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
checkFrontmatter(skillName, skillMdPath);
|
|
55
|
+
checkLinksInFile(skillName, skillMdPath);
|
|
56
|
+
checkEndpointsInFile(skillMdPath);
|
|
57
|
+
|
|
58
|
+
const flowsDir = join(skillPath, "flows");
|
|
59
|
+
if (existsSync(flowsDir) && statSync(flowsDir).isDirectory()) {
|
|
60
|
+
for (const entry of readdirSync(flowsDir, { withFileTypes: true })) {
|
|
61
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
62
|
+
const flowPath = join(flowsDir, entry.name);
|
|
63
|
+
checkLinksInFile(skillName, flowPath);
|
|
64
|
+
checkEndpointsInFile(flowPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function checkFrontmatter(skillName, skillMdPath) {
|
|
71
|
+
const raw = readFileSync(skillMdPath, "utf8");
|
|
72
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
73
|
+
const rel = relative(skillsDir, skillMdPath);
|
|
74
|
+
|
|
75
|
+
if (!match) {
|
|
76
|
+
errors.push(`${rel}: missing frontmatter (expected a leading --- ... --- block)`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const lines = match[1].split(/\r?\n/);
|
|
81
|
+
const nameLine = lines.find((line) => line.startsWith("name:"));
|
|
82
|
+
const descLine = lines.find((line) => line.startsWith("description:"));
|
|
83
|
+
|
|
84
|
+
if (!nameLine) {
|
|
85
|
+
errors.push(`${rel}: frontmatter has no 'name' field`);
|
|
86
|
+
} else {
|
|
87
|
+
const name = nameLine.slice("name:".length).trim();
|
|
88
|
+
if (name !== skillName) {
|
|
89
|
+
errors.push(`${rel}: name '${name}' does not match directory name '${skillName}'`);
|
|
90
|
+
}
|
|
91
|
+
if (name.length > NAME_LIMIT) {
|
|
92
|
+
errors.push(`${rel}: name is ${name.length} chars, over the ${NAME_LIMIT}-char limit`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!descLine) {
|
|
97
|
+
errors.push(`${rel}: frontmatter has no 'description' field`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const singleLineMatch = descLine.match(/^description:\s*"(.*)"\s*$/);
|
|
102
|
+
if (!singleLineMatch) {
|
|
103
|
+
errors.push(
|
|
104
|
+
`${rel}: 'description' must be a double-quoted string on a single physical line ` +
|
|
105
|
+
`(blocks-cli's frontmatter parser reads it line-by-line -- a wrapped description silently truncates)`
|
|
106
|
+
);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const description = singleLineMatch[1];
|
|
111
|
+
if (description.length === 0) {
|
|
112
|
+
errors.push(`${rel}: 'description' is empty`);
|
|
113
|
+
} else if (description.length > DESCRIPTION_HARD_LIMIT) {
|
|
114
|
+
errors.push(`${rel}: description is ${description.length} chars, over the ${DESCRIPTION_HARD_LIMIT}-char hard limit`);
|
|
115
|
+
} else if (description.length > DESCRIPTION_WARN_LIMIT) {
|
|
116
|
+
warnings.push(`${rel}: description is ${description.length} chars, over the ${DESCRIPTION_WARN_LIMIT}-char house-style target (aim for ~400-600)`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function checkLinksInFile(skillName, filePath) {
|
|
121
|
+
const raw = readFileSync(filePath, "utf8");
|
|
122
|
+
const rel = relative(skillsDir, filePath);
|
|
123
|
+
const linkPattern = /\[[^\]]*\]\(([^)]+)\)/g;
|
|
124
|
+
const fileDir = dirname(filePath);
|
|
125
|
+
|
|
126
|
+
for (const match of raw.matchAll(linkPattern)) {
|
|
127
|
+
const target = match[1].trim();
|
|
128
|
+
if (/^[a-z]+:\/\//i.test(target) || target.startsWith("#")) continue; // external URL or in-page anchor
|
|
129
|
+
|
|
130
|
+
const [pathPart] = target.split("#");
|
|
131
|
+
if (!pathPart) continue;
|
|
132
|
+
|
|
133
|
+
const resolved = join(fileDir, pathPart);
|
|
134
|
+
if (!existsSync(resolved)) {
|
|
135
|
+
errors.push(`${rel}: broken link to '${pathPart}' (resolved: ${relative(skillsDir, resolved)})`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const resolvedRelToSkills = relative(skillsDir, resolved).split(/[\\/]/);
|
|
140
|
+
const targetSkill = resolvedRelToSkills[0];
|
|
141
|
+
if (targetSkill !== skillName) {
|
|
142
|
+
errors.push(
|
|
143
|
+
`${rel}: link leaves this skill's own directory ('${pathPart}') -- ` +
|
|
144
|
+
`mention other skills by name in plain text instead, never a link, since ` +
|
|
145
|
+
`'blocks skill add' only copies one skill directory at a time and the target isn't guaranteed to be present`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function checkEndpointsInFile(filePath) {
|
|
152
|
+
const raw = readFileSync(filePath, "utf8");
|
|
153
|
+
const rel = relative(skillsDir, filePath);
|
|
154
|
+
|
|
155
|
+
for (const match of raw.matchAll(ENDPOINT_PATTERN)) {
|
|
156
|
+
errors.push(`${rel}: raw API endpoint path '${match[0]}' -- describe the CLI command/SDK method instead, never the wire path`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const warning of warnings) console.warn(`warning: ${warning}`);
|
|
161
|
+
if (errors.length === 0) {
|
|
162
|
+
console.log(`ok: ${skillDirs.length} skills, no problems found${warnings.length ? ` (${warnings.length} warning(s) above)` : ""}`);
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.error(`${errors.length} problem(s) found:`);
|
|
167
|
+
for (const error of errors) console.error(` - ${error}`);
|
|
168
|
+
process.exit(1);
|