@pikku/inspector 0.12.112 → 0.12.113

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.
@@ -1,5 +1,5 @@
1
1
  import * as ts from 'typescript';
2
- import { existsSync, readFileSync, readdirSync } from 'fs';
2
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
3
3
  import { dirname, join } from 'path';
4
4
  import { ErrorCode } from '../error-codes.js';
5
5
  const SCOPES = ['studio', 'team', 'project'];
@@ -110,6 +110,7 @@ const readScreen = (element, fail) => {
110
110
  let scopes;
111
111
  let component;
112
112
  let app;
113
+ let agent;
113
114
  let ok = true;
114
115
  for (const field of element.properties) {
115
116
  const key = propName(field);
@@ -136,6 +137,8 @@ const readScreen = (element, fail) => {
136
137
  read = replaces = booleanLiteral(value);
137
138
  else if (key === 'needsApp')
138
139
  read = needsApp = booleanLiteral(value);
140
+ else if (key === 'agent')
141
+ read = agent = ts.isStringLiteral(value) ? value.text : undefined;
139
142
  else if (key === 'layout')
140
143
  read = layout = oneOf(value, ['bare']);
141
144
  else if (key === 'scopes') {
@@ -182,6 +185,7 @@ const readScreen = (element, fail) => {
182
185
  ...(needsApp !== undefined ? { needsApp } : {}),
183
186
  ...(layout ? { layout } : {}),
184
187
  ...(scopes ? { scopes } : {}),
188
+ ...(agent !== undefined ? { agent } : {}),
185
189
  ...(component ? { component } : { app: app }),
186
190
  };
187
191
  };
@@ -195,7 +199,8 @@ export function addPikkuStudio(node, state, logger) {
195
199
  if (!ts.isCallExpression(node))
196
200
  return;
197
201
  const { expression, arguments: args } = node;
198
- if (!ts.isIdentifier(expression) || expression.text !== 'definePlugin' && expression.text !== 'pikkuStudio')
202
+ if (!ts.isIdentifier(expression) ||
203
+ (expression.text !== 'definePlugin' && expression.text !== 'pikkuStudio'))
199
204
  return;
200
205
  const [firstArg] = args;
201
206
  if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
@@ -259,6 +264,34 @@ const packageRoot = (file) => {
259
264
  dir = parent;
260
265
  }
261
266
  };
267
+ const AGENT_NAME = /^[a-z][a-z0-9-]*$/;
268
+ const STUDIO_AGENT = /^studio:[a-z][a-z0-9-]*$/;
269
+ /** The text of a file that exists and is not blank, else undefined. */
270
+ const readNonEmpty = (file) => {
271
+ try {
272
+ if (!statSync(file).isFile())
273
+ return undefined;
274
+ const text = readFileSync(file, 'utf-8');
275
+ return text.trim() === '' ? undefined : text;
276
+ }
277
+ catch {
278
+ return undefined;
279
+ }
280
+ };
281
+ /** The `key: value` lines of a leading `---` block; undefined when there is none. */
282
+ const readFrontmatter = (text) => {
283
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
284
+ if (!match)
285
+ return undefined;
286
+ const fields = {};
287
+ for (const line of match[1].split(/\r?\n/)) {
288
+ const pair = /^([A-Za-z][\w-]*):\s*(.*?)\s*$/.exec(line);
289
+ if (!pair)
290
+ continue;
291
+ fields[pair[1]] = pair[2].replace(/^(['"])(.*)\1$/, '$2');
292
+ }
293
+ return fields;
294
+ };
262
295
  const readCatalog = (file) => {
263
296
  try {
264
297
  return JSON.parse(readFileSync(file, 'utf-8'));
@@ -299,6 +332,46 @@ export function validatePikkuStudio(logger, state) {
299
332
  }
300
333
  }
301
334
  for (const screen of studio.screens) {
335
+ const agent = screen.agent;
336
+ if (agent !== undefined) {
337
+ const invalid = (reason) => logger.critical(ErrorCode.STUDIO_SCREEN_AGENT_INVALID, `Screen '${screen.path}' has the agent '${agent}', ${reason}`);
338
+ if (agent.startsWith('studio:')) {
339
+ // An agent Studio ships: pikku cannot see it, so only the format is checked.
340
+ if (!STUDIO_AGENT.test(agent)) {
341
+ invalid("which is not a valid agent: use a name for an agent the addon ships (lowercase letters, digits and hyphens, starting with a letter) or 'studio:<name>' for one Studio ships");
342
+ }
343
+ }
344
+ else if (!AGENT_NAME.test(agent)) {
345
+ invalid("which is not a valid agent: use a name for an agent the addon ships (lowercase letters, digits and hyphens, starting with a letter) or 'studio:<name>' for one Studio ships");
346
+ }
347
+ else {
348
+ const text = root
349
+ ? readNonEmpty(join(root, 'agents', `${agent}.md`))
350
+ : undefined;
351
+ const fields = text === undefined ? undefined : readFrontmatter(text);
352
+ if (text === undefined) {
353
+ invalid(`but the addon has no non-empty agents/${agent}.md`);
354
+ }
355
+ else if (!fields) {
356
+ invalid(`whose agents/${agent}.md has no frontmatter; it needs a title and a description`);
357
+ }
358
+ else {
359
+ for (const field of ['title', 'description']) {
360
+ const key = fields[field];
361
+ if (!key) {
362
+ invalid(`whose agents/${agent}.md frontmatter has no ${field}; it must be a message key`);
363
+ }
364
+ else if (!en || !Object.hasOwn(en, key)) {
365
+ logger.critical(ErrorCode.STUDIO_MESSAGE_KEY_MISSING, `definePlugin uses the message key '${key}', which is missing from the addon's messages/en.json`);
366
+ }
367
+ else if (field === 'title')
368
+ screen.agentTitle = key;
369
+ else
370
+ screen.agentDescription = key;
371
+ }
372
+ }
373
+ }
374
+ }
302
375
  const badge = screen.nav?.badge;
303
376
  if (badge !== undefined && !state.functions.meta[badge]) {
304
377
  logger.critical(ErrorCode.STUDIO_BADGE_FUNCTION_NOT_FOUND, `Screen '${screen.path}' has the nav badge '${badge}', which names no function in this addon`);
@@ -57,6 +57,7 @@ export declare enum ErrorCode {
57
57
  ADDON_CONTRIBUTORS_NOT_STATIC = "PKU362",
58
58
  ADDON_CONTRIBUTOR_UNKNOWN = "PKU363",
59
59
  ADDON_LEGACY_CONTRIBUTORS = "PKU364",
60
+ STUDIO_SCREEN_AGENT_INVALID = "PKU365",
60
61
  DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
61
62
  DB_JSON_COLUMN_UNTYPED = "PKU481",
62
63
  DB_FORMAT_ON_NON_STRING = "PKU482",
@@ -60,6 +60,7 @@ export var ErrorCode;
60
60
  ErrorCode["ADDON_CONTRIBUTORS_NOT_STATIC"] = "PKU362";
61
61
  ErrorCode["ADDON_CONTRIBUTOR_UNKNOWN"] = "PKU363";
62
62
  ErrorCode["ADDON_LEGACY_CONTRIBUTORS"] = "PKU364";
63
+ ErrorCode["STUDIO_SCREEN_AGENT_INVALID"] = "PKU365";
63
64
  // Database schema codegen warnings
64
65
  ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
65
66
  ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";
package/dist/types.d.ts CHANGED
@@ -548,6 +548,11 @@ export type StudioScreenMeta = {
548
548
  needsApp?: boolean;
549
549
  layout?: 'bare';
550
550
  scopes?: string[];
551
+ agent?: string;
552
+ /** Message key from the agent file's frontmatter, filled by validation. */
553
+ agentTitle?: string;
554
+ /** Message key from the agent file's frontmatter, filled by validation. */
555
+ agentDescription?: string;
551
556
  } & ({
552
557
  component: string;
553
558
  app?: undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/inspector",
3
- "version": "0.12.112",
3
+ "version": "0.12.113",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -70,10 +70,10 @@
70
70
  "zod": "^4.6.7"
71
71
  },
72
72
  "peerDependencies": {
73
- "@pikku/core": "^0.12.148"
73
+ "@pikku/core": "^0.12.150"
74
74
  },
75
75
  "devDependencies": {
76
- "@pikku/core": "^0.12.148",
76
+ "@pikku/core": "^0.12.150",
77
77
  "@types/node": "^26.6.5"
78
78
  },
79
79
  "engines": {