@subbly/plugin-lint 0.0.7 → 0.0.8
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/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/package.json +6 -6
- package/src/cron.mjs +1 -1
- package/src/eslint-plugin.mjs +1 -1
- package/src/report.mjs +12 -12
- package/src/rules.mjs +37 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @subbly/plugin-lint
|
|
2
2
|
|
|
3
|
+
## 0.0.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Plugin automations are now workflows, as in the builder. The manifest key is `extensions["co.subbly.builder"].workflows` and the instruction files live in `co.subbly.builder/workflows/<slug>.md`. The rule ids `automation/*` are now `workflow/*`. A manifest that still declares `automations` fails rule `manifest/namespace-unknown-key`, as the builder refresh rejects it.
|
|
8
|
+
|
|
3
9
|
## 0.0.7
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ export default [
|
|
|
58
58
|
]
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
The gate is cross-file: whether a `plugin.json` is valid depends on `marketplace.json`, and a missing `
|
|
61
|
+
The gate is cross-file: whether a `plugin.json` is valid depends on `marketplace.json`, and a missing `workflows/<slug>.md` is a defect in a file that does not exist. ESLint visits one existing file at a time, so the engine runs once per invocation and hands each file the findings that belong to it. Every rule reaches the editor, absence checks included.
|
|
62
62
|
|
|
63
63
|
## Library
|
|
64
64
|
|
|
@@ -74,7 +74,7 @@ console.log(report.errors.length)
|
|
|
74
74
|
|
|
75
75
|
## What it checks
|
|
76
76
|
|
|
77
|
-
`marketplace.json` and every plugin manifest, `mcp.json` servers, skill and agent frontmatter,
|
|
77
|
+
`marketplace.json` and every plugin manifest, `mcp.json` servers, skill and agent frontmatter, workflow declarations against their files, cron floors, script lockfiles, plugin layout, and sandbox paths written in prose. Run `subbly-plugin-lint --rules` for the full list.
|
|
78
78
|
|
|
79
79
|
## Two severities
|
|
80
80
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@subbly/plugin-lint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "The Subbly marketplace release gate, offline. Lints marketplace.json, plugin manifests, mcp.json, skills, agents and
|
|
6
|
+
"description": "The Subbly marketplace release gate, offline. Lints marketplace.json, plugin manifests, mcp.json, skills, agents and workflows.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"author": "Subbly LTD <oleg@subbly.co> (https://www.subbly.co)",
|
|
9
9
|
"publishConfig": {
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"README.md",
|
|
32
32
|
"CHANGELOG.md"
|
|
33
33
|
],
|
|
34
|
-
"engines": {
|
|
35
|
-
"node": ">=20"
|
|
36
|
-
},
|
|
37
34
|
"scripts": {
|
|
38
35
|
"test": "node test/lint.test.mjs"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
39
|
}
|
|
40
|
-
}
|
|
40
|
+
}
|
package/src/cron.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Cron parsing and the builder 15-minute schedule floor.
|
|
2
2
|
//
|
|
3
|
-
// Mirrors packages/core/src/lib/
|
|
3
|
+
// Mirrors packages/core/src/lib/workflows/schedule.ts: 101 firings sampled
|
|
4
4
|
// from 2026-01-01T00:00:00Z, rejected if any gap falls under the floor.
|
|
5
5
|
|
|
6
6
|
export const SCHEDULE_FLOOR_MINUTES = 15
|
package/src/eslint-plugin.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ESLint bridge.
|
|
2
2
|
//
|
|
3
3
|
// The release gate is cross-file: whether plugin.json is valid depends on
|
|
4
|
-
// marketplace.json, and a missing
|
|
4
|
+
// marketplace.json, and a missing workflows/<slug>.md is a defect in a file
|
|
5
5
|
// that does not exist. ESLint visits one existing file at a time, so rules
|
|
6
6
|
// cannot be written that way directly.
|
|
7
7
|
//
|
package/src/report.mjs
CHANGED
|
@@ -50,18 +50,18 @@ export const RULES = {
|
|
|
50
50
|
'field/default-required': 'a default plugin cannot declare required fields',
|
|
51
51
|
'field/reserved-key': 'a field key must not restate the PLUGIN_ prefix the builder adds',
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
53
|
+
// workflows
|
|
54
|
+
'workflow/key-slug': 'workflow keys must be valid slugs',
|
|
55
|
+
'workflow/unknown-key': 'a declaration holds exactly name, schedule and model',
|
|
56
|
+
'workflow/name': 'name is required, 1-100 characters',
|
|
57
|
+
'workflow/schedule-syntax': 'schedule must be a cron expression the builder can parse',
|
|
58
|
+
'workflow/schedule-floor': 'schedule must fire at most every 15 minutes',
|
|
59
|
+
'workflow/schedule-unverified': 'this cron syntax cannot be checked offline',
|
|
60
|
+
'workflow/model': 'model must be normal or intelligent-high',
|
|
61
|
+
'workflow/missing-file': 'every declared workflow needs its markdown file',
|
|
62
|
+
'workflow/orphan-file': 'every workflow file needs a declaration',
|
|
63
|
+
'workflow/empty-file': 'a workflow file must not be empty',
|
|
64
|
+
'workflow/frontmatter': 'workflow frontmatter is not stripped and leaks into the instruction',
|
|
65
65
|
'setup/missing-install-skill': 'setup: true requires a skills/install skill',
|
|
66
66
|
|
|
67
67
|
// connectors and mcp.json
|
package/src/rules.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Mirrors subbly-builder branch feat/plugins-mcp:
|
|
4
4
|
// packages/plugins/src/marketplace/{marketplace-manifest,plugin-manifest,mcp-config}.ts
|
|
5
5
|
// packages/core/src/types/plugin-manifest.ts
|
|
6
|
-
// packages/core/src/lib/
|
|
6
|
+
// packages/core/src/lib/workflows/schedule.ts
|
|
7
7
|
//
|
|
8
8
|
// error = the refresh throws, so NO plugin in the marketplace releases.
|
|
9
9
|
// warn = the builder accepts it, but it is wrong, dead, or broken at runtime.
|
|
@@ -34,10 +34,10 @@ const CONNECTOR_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,49}$/
|
|
|
34
34
|
const CONTENT_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i
|
|
35
35
|
|
|
36
36
|
const MANIFEST_KEYS = ['$schema', 'name', 'version', 'description', 'author', 'homepage', 'repository', 'license', 'keywords', 'extensions']
|
|
37
|
-
const NAMESPACE_KEYS = ['displayName', 'image', 'default', 'fields', '
|
|
37
|
+
const NAMESPACE_KEYS = ['displayName', 'image', 'default', 'fields', 'workflows', 'setup', 'connectors']
|
|
38
38
|
const FIELD_KEYS = ['key', 'label', 'description', 'type', 'required', 'secret', 'public', 'pattern', 'options']
|
|
39
|
-
const
|
|
40
|
-
const
|
|
39
|
+
const WORKFLOW_KEYS = ['name', 'schedule', 'model']
|
|
40
|
+
const WORKFLOW_MODELS = ['normal', 'intelligent-high']
|
|
41
41
|
const SERVER_KEYS = ['type', 'url', 'headers']
|
|
42
42
|
|
|
43
43
|
export const AGENT_TOOLS = ['read_file', 'write_file', 'edit_file', 'apply_patch', 'execute_command', 'get_stock_image', 'web_fetch', 'web_search', 'generate_image', 'restart_preview', 'apply_to_preview', 'tool_run', 'tool_search', 'tool_describe', 'skill']
|
|
@@ -53,7 +53,7 @@ export const DEFAULT_ENV_KEYS = [
|
|
|
53
53
|
|
|
54
54
|
const RESERVED_SLUG = 'custom'
|
|
55
55
|
|
|
56
|
-
const MISPLACED = ['agents', '
|
|
56
|
+
const MISPLACED = ['agents', 'workflows', 'scripts', 'instructions.md']
|
|
57
57
|
|
|
58
58
|
// The builder-maintained symlink to the installed version, the only stable
|
|
59
59
|
// segment prose may use in a sandbox path.
|
|
@@ -409,7 +409,7 @@ function lintPluginManifest(root, entry, report) {
|
|
|
409
409
|
check.boolean('manifest/boolean', `${ns}.setup`, ext.setup)
|
|
410
410
|
|
|
411
411
|
const fields = lintFields(report, file, at, check, ns, ext, entry.slug)
|
|
412
|
-
const
|
|
412
|
+
const workflows = lintWorkflowDeclarations(report, file, at, check, ns, ext)
|
|
413
413
|
const connectors = lintConnectors(report, file, at, check, ns, ext)
|
|
414
414
|
|
|
415
415
|
if (ext.default === true && fields.some((f) => f.required === true)) {
|
|
@@ -421,7 +421,7 @@ function lintPluginManifest(root, entry, report) {
|
|
|
421
421
|
)
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
return { fieldKeys: fields.map((f) => f.key).filter(Boolean),
|
|
424
|
+
return { fieldKeys: fields.map((f) => f.key).filter(Boolean), workflows, connectors, setup: ext.setup === true }
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
function lintFields(report, file, at, check, ns, ext, slug) {
|
|
@@ -510,39 +510,39 @@ function lintFields(report, file, at, check, ns, ext, slug) {
|
|
|
510
510
|
return ext.fields.filter(isObject)
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
-
function
|
|
514
|
-
if (ext.
|
|
515
|
-
if (!isObject(ext.
|
|
516
|
-
report.error('
|
|
513
|
+
function lintWorkflowDeclarations(report, file, at, check, ns, ext) {
|
|
514
|
+
if (ext.workflows === undefined) return []
|
|
515
|
+
if (!isObject(ext.workflows)) {
|
|
516
|
+
report.error('workflow/key-slug', file, `${ns}.workflows: expected object keyed by slug, received ${typeOf(ext.workflows)}`, at(`${ns}.workflows`))
|
|
517
517
|
return []
|
|
518
518
|
}
|
|
519
519
|
|
|
520
520
|
const slugs = []
|
|
521
|
-
for (const [slug, decl] of Object.entries(ext.
|
|
522
|
-
const p = `${ns}.
|
|
523
|
-
const label = `
|
|
521
|
+
for (const [slug, decl] of Object.entries(ext.workflows)) {
|
|
522
|
+
const p = `${ns}.workflows.${slug}`
|
|
523
|
+
const label = `workflows["${slug}"]`
|
|
524
524
|
|
|
525
525
|
if (slug.length < 1 || slug.length > 64 || !SLUG_PATTERN.test(slug)) {
|
|
526
|
-
report.error('
|
|
526
|
+
report.error('workflow/key-slug', file, `${label}: key ${SLUG_MESSAGE}`, at(p, { key: true }))
|
|
527
527
|
continue
|
|
528
528
|
}
|
|
529
529
|
if (!isObject(decl)) {
|
|
530
|
-
report.error('
|
|
530
|
+
report.error('workflow/unknown-key', file, `${label}: expected object, received ${typeOf(decl)}`, at(p))
|
|
531
531
|
continue
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
-
check.strictKeys('
|
|
535
|
-
check.string('
|
|
534
|
+
check.strictKeys('workflow/unknown-key', p, decl, WORKFLOW_KEYS, label)
|
|
535
|
+
check.string('workflow/name', `${p}.name`, decl.name, { min: 1, max: 100, trim: true, required: true, label: `${label}.name` })
|
|
536
536
|
|
|
537
|
-
if (check.string('
|
|
537
|
+
if (check.string('workflow/schedule-syntax', `${p}.schedule`, decl.schedule, { min: 1, max: 100, required: true, label: `${label}.schedule` })) {
|
|
538
538
|
lintSchedule(report, file, at(`${p}.schedule`), label, decl.schedule)
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
-
if (!
|
|
541
|
+
if (!WORKFLOW_MODELS.includes(decl.model)) {
|
|
542
542
|
report.error(
|
|
543
|
-
'
|
|
543
|
+
'workflow/model',
|
|
544
544
|
file,
|
|
545
|
-
`${label}.model: expected ${
|
|
545
|
+
`${label}.model: expected ${WORKFLOW_MODELS.join(' | ')}, received ${JSON.stringify(decl.model)}`,
|
|
546
546
|
at(`${p}.model`)
|
|
547
547
|
)
|
|
548
548
|
}
|
|
@@ -557,17 +557,17 @@ function lintSchedule(report, file, pos, label, schedule) {
|
|
|
557
557
|
cron = parseCron(schedule)
|
|
558
558
|
} catch (e) {
|
|
559
559
|
if (e instanceof CronUnsupported) {
|
|
560
|
-
report.warn('
|
|
560
|
+
report.warn('workflow/schedule-unverified', file, `${label}.schedule contains "${e.message}", which cannot be checked offline`, {
|
|
561
561
|
...pos,
|
|
562
562
|
hint: 'the builder may accept it; confirm the 15-minute floor by hand',
|
|
563
563
|
})
|
|
564
564
|
return
|
|
565
565
|
}
|
|
566
|
-
report.error('
|
|
566
|
+
report.error('workflow/schedule-syntax', file, `${label}.schedule: invalid cron expression (${e.message})`, pos)
|
|
567
567
|
return
|
|
568
568
|
}
|
|
569
569
|
if (!meetsFloor(cron)) {
|
|
570
|
-
report.error('
|
|
570
|
+
report.error('workflow/schedule-floor', file, `${label}.schedule must fire at most every ${SCHEDULE_FLOOR_MINUTES} minutes`, pos)
|
|
571
571
|
}
|
|
572
572
|
}
|
|
573
573
|
|
|
@@ -976,10 +976,10 @@ function lintAgents(root, entry, report) {
|
|
|
976
976
|
}
|
|
977
977
|
}
|
|
978
978
|
|
|
979
|
-
// ---
|
|
979
|
+
// --- workflows -----------------------------------------------------------
|
|
980
980
|
|
|
981
|
-
function
|
|
982
|
-
const dir = join(entry.dir, NAMESPACE, '
|
|
981
|
+
function lintWorkflowFiles(root, entry, declared, report) {
|
|
982
|
+
const dir = join(entry.dir, NAMESPACE, 'workflows')
|
|
983
983
|
const present = new Set()
|
|
984
984
|
|
|
985
985
|
for (const dirent of listDir(dir)) {
|
|
@@ -991,10 +991,10 @@ function lintAutomationFiles(root, entry, declared, report) {
|
|
|
991
991
|
const orphans = [...present].filter((s) => !declared.includes(s))
|
|
992
992
|
for (const orphan of orphans) {
|
|
993
993
|
report.error(
|
|
994
|
-
'
|
|
994
|
+
'workflow/orphan-file',
|
|
995
995
|
relative(root, join(dir, `${orphan}.md`)),
|
|
996
|
-
`Invalid
|
|
997
|
-
{ line: 1, hint: `declare it in plugin.json under extensions["${NAMESPACE}"].
|
|
996
|
+
`Invalid workflows of ${entry.slug}: files without a manifest declaration: ${orphans.join(', ')}`,
|
|
997
|
+
{ line: 1, hint: `declare it in plugin.json under extensions["${NAMESPACE}"].workflows, or delete it` }
|
|
998
998
|
)
|
|
999
999
|
}
|
|
1000
1000
|
|
|
@@ -1002,19 +1002,19 @@ function lintAutomationFiles(root, entry, declared, report) {
|
|
|
1002
1002
|
const path = join(dir, `${slug}.md`)
|
|
1003
1003
|
if (!present.has(slug)) {
|
|
1004
1004
|
report.error(
|
|
1005
|
-
'
|
|
1005
|
+
'workflow/missing-file',
|
|
1006
1006
|
relative(root, join(entry.dir, 'plugin.json')),
|
|
1007
|
-
`Invalid
|
|
1008
|
-
{ hint: `create ${NAMESPACE}/
|
|
1007
|
+
`Invalid workflows of ${entry.slug}: missing workflows/${slug}.md`,
|
|
1008
|
+
{ hint: `create ${NAMESPACE}/workflows/${slug}.md` }
|
|
1009
1009
|
)
|
|
1010
1010
|
continue
|
|
1011
1011
|
}
|
|
1012
1012
|
const text = readIfExists(path) ?? ''
|
|
1013
1013
|
if (text.trim() === '') {
|
|
1014
|
-
report.error('
|
|
1014
|
+
report.error('workflow/empty-file', relative(root, path), `Invalid workflows of ${entry.slug}: workflows/${slug}.md is empty`, { line: 1 })
|
|
1015
1015
|
}
|
|
1016
1016
|
if (text.trimStart().startsWith('---')) {
|
|
1017
|
-
report.warn('
|
|
1017
|
+
report.warn('workflow/frontmatter', relative(root, path), 'frontmatter here is not parsed or stripped, so it lands verbatim in the agent instruction', {
|
|
1018
1018
|
line: 1,
|
|
1019
1019
|
hint: 'name, schedule and model belong in plugin.json',
|
|
1020
1020
|
})
|
|
@@ -1179,7 +1179,7 @@ export function lint(root, { git = true } = {}) {
|
|
|
1179
1179
|
lintMcp(root, entry, manifest, report)
|
|
1180
1180
|
lintSkills(root, entry, report)
|
|
1181
1181
|
lintAgents(root, entry, report)
|
|
1182
|
-
|
|
1182
|
+
lintWorkflowFiles(root, entry, manifest?.workflows ?? [], report)
|
|
1183
1183
|
lintSetup(root, entry, manifest, report)
|
|
1184
1184
|
lintLayout(root, entry, report)
|
|
1185
1185
|
}
|