@goodboyjs/cli 0.1.0 → 0.1.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.
@@ -4,7 +4,7 @@ import { appendFile, readFile } from 'node:fs/promises';
4
4
  import { join, sep } from 'node:path';
5
5
  import ora from 'ora';
6
6
  import { createRegistryAdapter } from '../lib/registry-adapter.js';
7
- import { readManifest, validateManifest } from '../lib/manifest.js';
7
+ import { readManifest, validateManifestDetailed } from '../lib/manifest.js';
8
8
  import { requestConsent } from '../lib/consent.js';
9
9
  import { scanForSymlinks } from '../lib/fs-security.js';
10
10
  import { logger, sanitiseError } from '../lib/logger.js';
@@ -73,7 +73,11 @@ export async function installNamed(name, options, cwd) {
73
73
  let manifest;
74
74
  try {
75
75
  const data = await readManifest(join(skillPath, 'manifest.json'));
76
- manifest = validateManifest(data);
76
+ const detailed = validateManifestDetailed(data);
77
+ manifest = detailed.manifest;
78
+ for (const warning of detailed.warnings) {
79
+ logger.warn(warning);
80
+ }
77
81
  }
78
82
  catch (err) {
79
83
  spinner.fail('Manifest validation failed');
@@ -1,2 +1,7 @@
1
1
  import { Command } from 'commander';
2
+ export interface UpgradeOptions {
3
+ global?: boolean;
4
+ }
5
+ export declare function upgradeSkill(name: string, options: UpgradeOptions, cwd: string): Promise<void>;
6
+ export declare function upgradeAll(options: UpgradeOptions, cwd: string): Promise<void>;
2
7
  export declare const upgradeCommand: Command;
@@ -3,7 +3,7 @@ import { cpSync, existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import ora from 'ora';
5
5
  import { createRegistryAdapter } from '../lib/registry-adapter.js';
6
- import { readManifest, validateManifest } from '../lib/manifest.js';
6
+ import { readManifest, validateManifestDetailed } from '../lib/manifest.js';
7
7
  import { scanForSymlinks } from '../lib/fs-security.js';
8
8
  import { logger, sanitiseError } from '../lib/logger.js';
9
9
  import { SKILL_NAME_RE } from '../lib/validation.js';
@@ -12,7 +12,7 @@ import { getStorePath, getGoodboyHome } from '../lib/store.js';
12
12
  function getProjectSkillsPath(cwd) {
13
13
  return join(cwd, '.claude', 'skills');
14
14
  }
15
- async function upgradeSkill(name, options, cwd) {
15
+ export async function upgradeSkill(name, options, cwd) {
16
16
  if (!SKILL_NAME_RE.test(name)) {
17
17
  throw new Error(`Invalid skill name: "${name}". Must match ^[a-z0-9-]+$.`);
18
18
  }
@@ -29,7 +29,11 @@ async function upgradeSkill(name, options, cwd) {
29
29
  let manifest;
30
30
  try {
31
31
  const data = await readManifest(join(skillPath, 'manifest.json'));
32
- manifest = validateManifest(data);
32
+ const detailed = validateManifestDetailed(data);
33
+ manifest = detailed.manifest;
34
+ for (const warning of detailed.warnings) {
35
+ logger.warn(warning);
36
+ }
33
37
  }
34
38
  catch (err) {
35
39
  spinner.fail('Manifest validation failed');
@@ -67,7 +71,7 @@ async function upgradeSkill(name, options, cwd) {
67
71
  const from = lockedVersion !== null ? `${lockedVersion} → ` : '';
68
72
  spinner.succeed(`Upgraded "${name}" (${from}${manifest.version})`);
69
73
  }
70
- async function upgradeAll(options, cwd) {
74
+ export async function upgradeAll(options, cwd) {
71
75
  const dir = options.global ? getGoodboyHome() : cwd;
72
76
  const goodboy = await readGoodBoyJson(dir);
73
77
  if (!goodboy) {
@@ -1,4 +1,22 @@
1
1
  import type { GoodBoyManifest } from '../types/index.js';
2
+ /**
3
+ * The manifest schema version this CLI validates strictly against. Manifests
4
+ * declaring a newer minor are tolerated (unknown top-level fields stripped,
5
+ * with a warning); a newer major is rejected. See validateManifestDetailed().
6
+ */
7
+ export declare const KNOWN_SCHEMA_VERSION = "1.0.0";
2
8
  export declare function readManifest(filePath: string): Promise<unknown>;
9
+ export interface ManifestValidationResult {
10
+ manifest: GoodBoyManifest;
11
+ warnings: string[];
12
+ }
13
+ /**
14
+ * Validates a manifest, tolerating a newer-minor `schema_version` than this
15
+ * CLI knows: unknown top-level fields are stripped (never mutating the
16
+ * caller's object) and a warning is returned instead of a hard failure. A
17
+ * newer major is always rejected. Manifests at or below the known minor take
18
+ * the exact same strict path as before this tolerance existed.
19
+ */
20
+ export declare function validateManifestDetailed(data: unknown): ManifestValidationResult;
3
21
  export declare function validateManifest(data: unknown): GoodBoyManifest;
4
22
  export declare function writeManifest(filePath: string, data: GoodBoyManifest): Promise<void>;
@@ -7,16 +7,42 @@ const addFormats = addFormatsPkg.default;
7
7
  const _require = createRequire(import.meta.url);
8
8
  const MAX_MANIFEST_BYTES = 512 * 1024; // 512 KB
9
9
  const MAX_NESTING_DEPTH = 10;
10
+ /**
11
+ * The manifest schema version this CLI validates strictly against. Manifests
12
+ * declaring a newer minor are tolerated (unknown top-level fields stripped,
13
+ * with a warning); a newer major is rejected. See validateManifestDetailed().
14
+ */
15
+ export const KNOWN_SCHEMA_VERSION = '1.0.0';
16
+ const SCHEMA_VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/;
17
+ let _schema = null;
10
18
  let _validator = null;
19
+ function getSchema() {
20
+ if (_schema)
21
+ return _schema;
22
+ _schema = _require('@goodboyjs/schema/src/manifest.schema.json');
23
+ return _schema;
24
+ }
11
25
  function getValidator() {
12
26
  if (_validator)
13
27
  return _validator;
14
28
  const ajv = new Ajv({ strict: true, allErrors: true });
15
29
  addFormats(ajv);
16
- const schema = _require('@goodboyjs/schema/src/manifest.schema.json');
17
- _validator = ajv.compile(schema);
30
+ _validator = ajv.compile(getSchema());
18
31
  return _validator;
19
32
  }
33
+ // Top-level property names the known schema recognizes. Used only to strip
34
+ // unknown top-level fields from a newer-minor manifest before strict
35
+ // validation — never to loosen validation of known fields.
36
+ function getKnownTopLevelKeys() {
37
+ /* c8 ignore next -- the shipped schema always has a root "properties" key; ?? fallback is unreachable. Fail-closed if ever violated: an empty Set would strip every field, and every existing manifest test would fail immediately. */
38
+ const properties = (getSchema()['properties'] ?? {});
39
+ return new Set(Object.keys(properties));
40
+ }
41
+ function throwValidationError(validate) {
42
+ /* c8 ignore next 2 -- ajv always populates errors[] after a failed validate(); ?? fallbacks are unreachable */
43
+ const lines = (validate.errors ?? []).map((e) => ` ${e.instancePath || '(root)'}: ${e.message ?? 'validation failed'}`);
44
+ throw new Error(`Invalid manifest:\n${lines.join('\n')}`);
45
+ }
20
46
  // Heuristic nesting depth check: counts opening brackets/braces.
21
47
  // This is intentionally fast and runs before JSON.parse() to guard against
22
48
  // deeply nested payloads that could exhaust the stack. Brackets inside
@@ -66,14 +92,61 @@ export async function readManifest(filePath) {
66
92
  throw new Error(`manifest.json contains invalid JSON`);
67
93
  }
68
94
  }
69
- export function validateManifest(data) {
95
+ /**
96
+ * Validates a manifest, tolerating a newer-minor `schema_version` than this
97
+ * CLI knows: unknown top-level fields are stripped (never mutating the
98
+ * caller's object) and a warning is returned instead of a hard failure. A
99
+ * newer major is always rejected. Manifests at or below the known minor take
100
+ * the exact same strict path as before this tolerance existed.
101
+ */
102
+ export function validateManifestDetailed(data) {
70
103
  const validate = getValidator();
71
- if (!validate(data)) {
72
- /* c8 ignore next 2 -- ajv always populates errors[] after a failed validate(); ?? fallbacks are unreachable */
73
- const lines = (validate.errors ?? []).map((e) => ` ${e.instancePath || '(root)'}: ${e.message ?? 'validation failed'}`);
74
- throw new Error(`Invalid manifest:\n${lines.join('\n')}`);
104
+ const knownParts = KNOWN_SCHEMA_VERSION.split('.').map(Number);
105
+ const knownMajor = knownParts[0];
106
+ const knownMinor = knownParts[1];
107
+ if (data !== null && typeof data === 'object' && !Array.isArray(data)) {
108
+ const rawVersion = data['schema_version'];
109
+ // The length gate (mirrors the schema's own maxLength: 32) must run before any
110
+ // interpolation of rawVersion into a message: an overlong value falls straight
111
+ // through to strict Ajv validation, which rejects it via maxLength without ever
112
+ // embedding the value in the error text.
113
+ if (typeof rawVersion === 'string' && rawVersion.length <= 32) {
114
+ const match = SCHEMA_VERSION_PATTERN.exec(rawVersion);
115
+ if (match) {
116
+ const major = Number(match[1]);
117
+ const minor = Number(match[2]);
118
+ if (major !== knownMajor) {
119
+ const upgradeHint = major > knownMajor ? ' Upgrade GoodBoy to use this skill.' : '';
120
+ throw new Error(`manifest declares schema ${rawVersion}; this version of GoodBoy supports ${knownMajor}.x manifests.${upgradeHint}`);
121
+ }
122
+ if (minor > knownMinor) {
123
+ const known = getKnownTopLevelKeys();
124
+ const stripped = {};
125
+ for (const [key, value] of Object.entries(data)) {
126
+ if (known.has(key))
127
+ stripped[key] = value;
128
+ }
129
+ if (!validate(stripped))
130
+ throwValidationError(validate);
131
+ return {
132
+ manifest: stripped,
133
+ warnings: [
134
+ `manifest uses schema ${rawVersion}; this GoodBoy CLI knows ${KNOWN_SCHEMA_VERSION}. Unknown fields were ignored — upgrade GoodBoy to use them.`,
135
+ ],
136
+ };
137
+ }
138
+ // minor <= knownMinor: fall through to strict validation below, unchanged.
139
+ }
140
+ // non-matching string: fall through; Ajv's pattern check reports the standard error.
141
+ }
142
+ // missing/non-string schema_version: fall through; Ajv's required/type check reports the standard error.
75
143
  }
76
- return data;
144
+ if (!validate(data))
145
+ throwValidationError(validate);
146
+ return { manifest: data, warnings: [] };
147
+ }
148
+ export function validateManifest(data) {
149
+ return validateManifestDetailed(data).manifest;
77
150
  }
78
151
  export async function writeManifest(filePath, data) {
79
152
  const resolved = resolve(filePath);
@@ -1,7 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { readManifest, validateManifest } from './manifest.js';
4
+ import { readManifest, validateManifestDetailed } from './manifest.js';
5
5
  import { logger } from './logger.js';
6
6
  function parseFrontmatter(content) {
7
7
  const lines = content.split('\n');
@@ -40,7 +40,11 @@ export async function validateSkillDirectory(skillPath) {
40
40
  try {
41
41
  const raw = await readManifest(manifestPath);
42
42
  try {
43
- manifest = validateManifest(raw);
43
+ const detailed = validateManifestDetailed(raw);
44
+ manifest = detailed.manifest;
45
+ for (const warning of detailed.warnings) {
46
+ issues.push({ severity: 'warning', message: warning });
47
+ }
44
48
  }
45
49
  catch (err) {
46
50
  /* c8 ignore next */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodboyjs/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A personal skill registry and package manager for Claude Code and the Agent Skills ecosystem",
5
5
  "keywords": [
6
6
  "claude-code",