structured-context 0.15.0 → 0.16.0

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 CHANGED
@@ -188,6 +188,14 @@ Generates a Mermaid `graph TD` diagram from validated space nodes:
188
188
  - Handles orphan nodes (no parent) as a separate cluster
189
189
  - Outputs to file or stdout
190
190
 
191
+ ### List schemas
192
+
193
+ ```bash
194
+ sctx schemas list [--json]
195
+ ```
196
+
197
+ Lists the bundled schemas, schemas contributed by plugins, and any other schema file your config uses, with the spaces that use each. `--json` gives the data that `listSchemas` returns; see [docs/api.md](docs/api.md).
198
+
191
199
  ### Show schema ERD
192
200
 
193
201
  ```bash
package/dist/api.d.ts CHANGED
@@ -27,6 +27,7 @@ export type { ParseHook, ParseResult, PluginContext, RenderFileOutput, RenderFor
27
27
  export { isRenderMultiFileOutput, resolvePluginDir, resolvePluginSchemasDir, resolveSpacePluginSchemas, } from './plugins/util';
28
28
  export { readSpace } from './read/read-space';
29
29
  export { buildTargetIndex, formatWikilink, type ParsedWikilink, parseWikilink, wikilinkToTarget, } from './read/wikilink-utils';
30
+ export { listSchemas } from './schema/list-schemas';
30
31
  export type { SharedEmbeddingFields } from './schema/metadata-contract';
31
32
  export { bundledSchemasDir, loadSchema, type PluginSchemaSource, setBundledSchemasDir } from './schema/schema';
32
33
  export { createSpaceContext, SpaceNotFoundError } from './space-context';
package/dist/api.js CHANGED
@@ -1,3 +1,4 @@
1
+ // Public entry point: do not add exports without the maintainer's agreement (AGENTS.md, "Public API surface").
1
2
  /**
2
3
  * Public API for structured-context.
3
4
  *
@@ -24,6 +25,7 @@ export { assembleSpaceGraph, loadSpaceGraph } from './load-space-graph';
24
25
  export { isRenderMultiFileOutput, resolvePluginDir, resolvePluginSchemasDir, resolveSpacePluginSchemas, } from './plugins/util';
25
26
  export { readSpace } from './read/read-space';
26
27
  export { buildTargetIndex, formatWikilink, parseWikilink, wikilinkToTarget, } from './read/wikilink-utils';
28
+ export { listSchemas } from './schema/list-schemas';
27
29
  export { bundledSchemasDir, loadSchema, setBundledSchemasDir } from './schema/schema';
28
30
  export { createSpaceContext, SpaceNotFoundError } from './space-context';
29
31
  export { buildSpaceGraph } from './space-graph';
@@ -1,4 +1,6 @@
1
- export declare function listSchemas(): Promise<void>;
1
+ export declare function printSchemas(options?: {
2
+ json?: boolean;
3
+ }): Promise<void>;
2
4
  export declare function showSchema(file: string | undefined, options: {
3
5
  space?: string;
4
6
  raw: boolean;
@@ -1,16 +1,14 @@
1
1
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
- import { dirname, join, resolve } from 'node:path';
2
+ import { join, resolve } from 'node:path';
3
3
  import { loadConfig } from '../config';
4
4
  import { discoverPlugins } from '../plugins/loader';
5
5
  import { normalizePluginName, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from '../plugins/util';
6
+ import { listSchemas } from '../schema/list-schemas';
6
7
  import { ruleSemantic } from '../schema/rule-severity';
7
8
  import { bundledSchemasDir, extractEntityInfo, loadSchema, readRawSchema, } from '../schema/schema';
8
9
  import { mergeVariantProperties, resolveRef } from '../schema/schema-refs';
9
10
  import { DEFAULT_SEVERITY_BY_CATEGORY } from '../schema/validate-rules';
10
11
  import { createSpaceContext } from '../space-context';
11
- function isBundledPath(schemaPath) {
12
- return dirname(schemaPath) === bundledSchemasDir;
13
- }
14
12
  function extractRefs(obj, refs) {
15
13
  if (typeof obj !== 'object' || obj === null)
16
14
  return;
@@ -207,57 +205,30 @@ function ruleAnnotations(rule) {
207
205
  notes.push(`when: ${rule.when}`);
208
206
  return notes.length > 0 ? ` [${notes.join('] [')}]` : '';
209
207
  }
210
- export async function listSchemas() {
211
- const config = loadConfig();
212
- // List all schemas known to config
213
- if (existsSync(bundledSchemasDir)) {
214
- const files = readdirSync(bundledSchemasDir)
215
- .filter((f) => f.endsWith('.json'))
216
- .sort();
217
- if (files.length > 0) {
218
- console.log('Bundled schemas:');
219
- for (const file of files) {
220
- const schema = readRawSchema(join(bundledSchemasDir, file));
221
- const id = typeof schema.$id === 'string' ? schema.$id : '(no $id)';
222
- console.log(` ${file}${file.startsWith('_') ? ' [partial]' : ''} (${id})`);
223
- }
224
- }
208
+ export async function printSchemas(options = {}) {
209
+ const listings = await listSchemas();
210
+ if (options.json) {
211
+ console.log(JSON.stringify(listings, null, 2));
212
+ return;
225
213
  }
226
- // List schemas provided by discovered plugins
227
- const plugins = await discoverPlugins();
228
- for (const p of plugins) {
229
- if (p.schemasDir && existsSync(p.schemasDir)) {
230
- const files = readdirSync(p.schemasDir)
231
- .filter((f) => f.endsWith('.json'))
232
- .sort();
233
- if (files.length > 0) {
234
- console.log(`\nPlugin schemas (${p.name}):`);
235
- for (const file of files) {
236
- const schema = readRawSchema(join(p.schemasDir, file));
237
- const id = typeof schema.$id === 'string' ? schema.$id : '(no $id)';
238
- console.log(` ${file}${file.startsWith('_') ? ' [partial]' : ''} (${id})`);
239
- }
214
+ const sources = [...new Set(listings.map((l) => l.source))].sort((a, b) => Number(b === 'core') - Number(a === 'core') || Number(a === 'configured') - Number(b === 'configured'));
215
+ for (const source of sources) {
216
+ const group = listings.filter((l) => l.source === source);
217
+ if (source === 'core')
218
+ console.log('Bundled schemas:');
219
+ else if (source === 'configured')
220
+ console.log('\nConfigured schemas:');
221
+ else
222
+ console.log(`\nPlugin schemas (${source}):`);
223
+ for (const l of group) {
224
+ if (source === 'configured')
225
+ console.log(` ${l.usedBy.join(', ')}: ${l.path ?? l.schema}${l.error ? ` [${l.error}]` : ''}`);
226
+ else {
227
+ const notes = [l.partial && '[partial]', l.id && `(${l.id})`, l.error && `[${l.error}]`].filter(Boolean);
228
+ console.log(` ${l.schema}${notes.length > 0 ? ` ${notes.join(' ')}` : ''}`);
240
229
  }
241
230
  }
242
231
  }
243
- const configured = [];
244
- const seen = new Set();
245
- if (config.schema && !isBundledPath(config.schema)) {
246
- configured.push({ source: 'global', path: config.schema });
247
- seen.add(config.schema);
248
- }
249
- for (const space of config.spaces) {
250
- if (space.schema && !seen.has(space.schema) && !isBundledPath(space.schema)) {
251
- configured.push({ source: space.name, path: space.schema });
252
- seen.add(space.schema);
253
- }
254
- }
255
- if (configured.length > 0) {
256
- console.log('\nConfigured schemas:');
257
- for (const { source, path } of configured) {
258
- console.log(` ${source}: ${path}`);
259
- }
260
- }
261
232
  }
262
233
  export async function showSchema(file, options) {
263
234
  const config = loadConfig();
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { dump } from './commands/dump';
8
8
  import { miroSyncCommand } from './commands/miro-sync';
9
9
  import { listPlugins } from './commands/plugins';
10
10
  import { render, renderList } from './commands/render';
11
- import { listSchemas, showSchema } from './commands/schemas';
11
+ import { printSchemas, showSchema } from './commands/schemas';
12
12
  import { show } from './commands/show';
13
13
  import { addSpaceCommand, listSpaces } from './commands/spaces';
14
14
  import { templateSync } from './commands/template-sync';
@@ -176,7 +176,8 @@ const schemasCmd = new Command('schemas').alias('schema').description('List and
176
176
  schemasCmd
177
177
  .command('list', { isDefault: true })
178
178
  .description('List available schemas')
179
- .action(async () => await listSchemas());
179
+ .option('--json', 'Output as JSON')
180
+ .action(async (options) => await printSchemas({ json: options.json ?? false }));
180
181
  schemasCmd
181
182
  .command('show')
182
183
  .description('Show schema structure (or raw JSON with --raw, or Mermaid ERD with --mermaid-erd)')
@@ -0,0 +1,27 @@
1
+ import { type Config } from '../config';
2
+ type SchemaListing = {
3
+ /** The value for a config's `schema` field. */
4
+ schema: string;
5
+ /** Absolute location or URL. Omitted for bundled schemas. */
6
+ path?: string;
7
+ /** `core`, a plugin's short name, or `configured`. */
8
+ source: string;
9
+ /** Names of the spaces that use it, and `global` when the top-level `schema` does. */
10
+ usedBy: string[];
11
+ partial: boolean;
12
+ id?: string;
13
+ title?: string;
14
+ description?: string;
15
+ /** Why the schema could not be read. */
16
+ error?: string;
17
+ };
18
+ /**
19
+ * List the schemas available to the config: bundled schemas, schemas contributed by plugins, and any other
20
+ * schema file the config points at. Each schema appears once, under the place its file lives.
21
+ */
22
+ export declare function listSchemas(options?: {
23
+ /** Loaded by discovery from `startDir` when omitted. */
24
+ config?: Config;
25
+ startDir?: string;
26
+ }): Promise<SchemaListing[]>;
27
+ export {};
@@ -0,0 +1,108 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
3
+ import { loadConfig, resolveConfigPath, resolveSchemaPath } from '../config';
4
+ import { PLUGIN_PREFIX } from '../constants';
5
+ import { CONFIG_PLUGINS_DIR, resolvePluginSchemasDir, resolveSpacePluginSchemas, shortenPluginName, } from '../plugins/util';
6
+ import { bundledSchemasDir, readRawSchema } from './schema';
7
+ function isRemote(ref) {
8
+ return /^https?:\/\//.test(ref);
9
+ }
10
+ function describeSchema(path) {
11
+ try {
12
+ const schema = readRawSchema(path);
13
+ return {
14
+ ...(typeof schema.$id === 'string' && { id: schema.$id }),
15
+ ...(typeof schema.title === 'string' && { title: schema.title }),
16
+ ...(typeof schema.description === 'string' && { description: schema.description }),
17
+ };
18
+ }
19
+ catch (e) {
20
+ return { error: e instanceof Error ? e.message : String(e) };
21
+ }
22
+ }
23
+ function jsonFiles(dir) {
24
+ if (!dir || !existsSync(dir))
25
+ return [];
26
+ return readdirSync(dir)
27
+ .filter((f) => f.endsWith('.json'))
28
+ .sort();
29
+ }
30
+ /**
31
+ * List the schemas available to the config: bundled schemas, schemas contributed by plugins, and any other
32
+ * schema file the config points at. Each schema appears once, under the place its file lives.
33
+ */
34
+ export async function listSchemas(options = {}) {
35
+ const config = options.config ?? loadConfig({ startDir: options.startDir });
36
+ const listings = [];
37
+ const byPath = new Map();
38
+ const add = (listing, location) => {
39
+ listings.push(listing);
40
+ byPath.set(resolve(location), listing);
41
+ };
42
+ for (const file of jsonFiles(bundledSchemasDir)) {
43
+ const location = join(bundledSchemasDir, file);
44
+ add({ schema: file, source: 'core', usedBy: [], partial: file.startsWith('_'), ...describeSchema(location) }, location);
45
+ }
46
+ // Relative schema references are anchored at the folder of the config that holds them
47
+ const fallbackDir = dirname(resolveConfigPath(options.startDir).path);
48
+ const configDirs = [...new Set([fallbackDir, ...config.spaces.map((s) => s.sourceDir ?? fallbackDir)])];
49
+ // Plugins are found the way schema references resolve them, without loading their code: those declared by a
50
+ // space or the config, and any installed beside a config file
51
+ const pluginSchemaDirs = new Map();
52
+ for (const space of [undefined, ...config.spaces]) {
53
+ for (const { pluginName, schemasDir } of resolveSpacePluginSchemas(space, config, space?.sourceDir ?? fallbackDir)) {
54
+ pluginSchemaDirs.set(pluginName, schemasDir);
55
+ }
56
+ }
57
+ for (const dir of configDirs) {
58
+ const pluginsDir = join(dir, CONFIG_PLUGINS_DIR);
59
+ if (!existsSync(pluginsDir))
60
+ continue;
61
+ for (const entry of readdirSync(pluginsDir).filter((e) => e.startsWith(PLUGIN_PREFIX))) {
62
+ const schemasDir = resolvePluginSchemasDir(entry, dir);
63
+ if (schemasDir && !pluginSchemaDirs.has(shortenPluginName(entry))) {
64
+ pluginSchemaDirs.set(shortenPluginName(entry), schemasDir);
65
+ }
66
+ }
67
+ }
68
+ for (const [source, schemasDir] of pluginSchemaDirs) {
69
+ for (const file of jsonFiles(schemasDir)) {
70
+ const location = join(schemasDir, file);
71
+ add({
72
+ schema: `${source}/${file}`,
73
+ path: location,
74
+ source,
75
+ usedBy: [],
76
+ partial: file.startsWith('_'),
77
+ ...describeSchema(location),
78
+ }, location);
79
+ }
80
+ }
81
+ const references = [
82
+ ...(config.schema ? [{ user: 'global', raw: config.schema, dir: fallbackDir }] : []),
83
+ ...config.spaces.flatMap((s) => s.schema ? [{ user: s.name, raw: s.schema, dir: s.sourceDir ?? fallbackDir }] : []),
84
+ ];
85
+ for (const { user, raw, dir } of references) {
86
+ const location = isRemote(raw) ? raw : resolveSchemaPath(raw, dir);
87
+ const key = isRemote(raw) ? raw : resolve(location);
88
+ let listing = byPath.get(key);
89
+ if (!listing) {
90
+ listing = {
91
+ schema: raw,
92
+ path: isRemote(raw) || isAbsolute(location) ? location : resolve(location),
93
+ source: 'configured',
94
+ usedBy: [],
95
+ partial: basename(raw).startsWith('_'),
96
+ ...(isRemote(raw)
97
+ ? { error: 'Remote schemas are not read' }
98
+ : existsSync(location)
99
+ ? describeSchema(location)
100
+ : { error: `Schema file not found: ${location}` }),
101
+ };
102
+ add(listing, key);
103
+ }
104
+ if (!listing.usedBy.includes(user))
105
+ listing.usedBy.push(user);
106
+ }
107
+ return listings;
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "structured-context",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Keep your content clean and your thinking clear with assistive tools for structured knowledge",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",