@emeryld/manager 1.1.0 → 1.2.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.
@@ -0,0 +1,83 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { rootDir } from '../helper-cli/env.js';
5
+ import { ROBOT_KINDS } from './types.js';
6
+ const SETTINGS_SECTION = 'manager.robot';
7
+ const workspaceSettingsPath = path.join(rootDir, '.vscode', 'settings.json');
8
+ export const DEFAULT_ROBOT_SETTINGS = {
9
+ includeKinds: [...ROBOT_KINDS],
10
+ exportedOnly: true,
11
+ maxColumns: 160,
12
+ };
13
+ function coerceNumber(value, fallback) {
14
+ const num = Number(value);
15
+ if (Number.isNaN(num) || num <= 0) {
16
+ return fallback;
17
+ }
18
+ return Math.floor(num);
19
+ }
20
+ function coerceBoolean(value, fallback) {
21
+ if (typeof value === 'boolean')
22
+ return value;
23
+ if (typeof value === 'string') {
24
+ if (value.toLowerCase() === 'true')
25
+ return true;
26
+ if (value.toLowerCase() === 'false')
27
+ return false;
28
+ }
29
+ return fallback;
30
+ }
31
+ function coerceKinds(value) {
32
+ const knownKinds = new Set(ROBOT_KINDS);
33
+ if (typeof value === 'string') {
34
+ const items = value
35
+ .split(',')
36
+ .map((chunk) => chunk.trim().toLowerCase())
37
+ .filter(Boolean);
38
+ const forced = [];
39
+ for (const entry of items) {
40
+ if (knownKinds.has(entry)) {
41
+ forced.push(entry);
42
+ }
43
+ }
44
+ if (forced.length > 0)
45
+ return forced;
46
+ }
47
+ if (Array.isArray(value)) {
48
+ const forced = [];
49
+ for (const entry of value) {
50
+ if (typeof entry === 'string') {
51
+ const normalized = entry.trim().toLowerCase();
52
+ if (knownKinds.has(normalized)) {
53
+ forced.push(normalized);
54
+ }
55
+ }
56
+ }
57
+ if (forced.length > 0)
58
+ return forced;
59
+ }
60
+ return DEFAULT_ROBOT_SETTINGS.includeKinds;
61
+ }
62
+ export async function loadRobotSettings() {
63
+ if (!existsSync(workspaceSettingsPath)) {
64
+ return DEFAULT_ROBOT_SETTINGS;
65
+ }
66
+ try {
67
+ const raw = await readFile(workspaceSettingsPath, 'utf-8');
68
+ const json = JSON.parse(raw);
69
+ const settings = json[SETTINGS_SECTION];
70
+ if (!settings || typeof settings !== 'object') {
71
+ return DEFAULT_ROBOT_SETTINGS;
72
+ }
73
+ const record = settings;
74
+ return {
75
+ includeKinds: coerceKinds(record.includeKinds ?? record.kinds),
76
+ exportedOnly: coerceBoolean(record.exportedOnly ?? DEFAULT_ROBOT_SETTINGS.exportedOnly, DEFAULT_ROBOT_SETTINGS.exportedOnly),
77
+ maxColumns: coerceNumber(record.maxColumns ?? DEFAULT_ROBOT_SETTINGS.maxColumns, DEFAULT_ROBOT_SETTINGS.maxColumns),
78
+ };
79
+ }
80
+ catch {
81
+ return DEFAULT_ROBOT_SETTINGS;
82
+ }
83
+ }
@@ -0,0 +1,98 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import * as ts from 'typescript';
3
+ import { colors } from '../utils/log.js';
4
+ import { exportReportLines, promptExportMode, } from '../utils/export.js';
5
+ import { collectSourceFiles } from '../format-checker/scan/collect.js';
6
+ import { resolveScriptKind } from '../format-checker/scan/utils.js';
7
+ import { rootDir } from '../helper-cli/env.js';
8
+ import { loadRobotSettings } from './config.js';
9
+ import { promptRobotSettings } from './cli/prompts.js';
10
+ import { collectFunctions } from './extractors/functions.js';
11
+ import { collectComponents } from './extractors/components.js';
12
+ import { collectConstants } from './extractors/constants.js';
13
+ import { collectTypes } from './extractors/types.js';
14
+ import { collectClasses } from './extractors/classes.js';
15
+ export async function runRobot() {
16
+ const defaults = await loadRobotSettings();
17
+ const settings = await promptRobotSettings(defaults);
18
+ const exportMode = await promptExportMode();
19
+ console.log(colors.magenta('Running robot analyzer (manager.robot settings)'));
20
+ const files = await collectSourceFiles(rootDir);
21
+ if (files.length === 0) {
22
+ console.log(colors.yellow('No source files found for robot'));
23
+ return {
24
+ functions: [],
25
+ components: [],
26
+ types: [],
27
+ consts: [],
28
+ classes: [],
29
+ };
30
+ }
31
+ console.log(colors.magenta(`Analyzing ${files.length} files for metadata`));
32
+ const result = {
33
+ functions: [],
34
+ components: [],
35
+ types: [],
36
+ consts: [],
37
+ classes: [],
38
+ };
39
+ for (const file of files) {
40
+ let content;
41
+ try {
42
+ content = await readFile(file, 'utf-8');
43
+ }
44
+ catch {
45
+ console.log(colors.yellow(`Unable to read ${file}`));
46
+ continue;
47
+ }
48
+ const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.ESNext, true, resolveScriptKind(file));
49
+ const context = { filePath: file, sourceFile };
50
+ const options = { settings, context };
51
+ if (settings.includeKinds.includes('function')) {
52
+ result.functions.push(...collectFunctions(options));
53
+ }
54
+ if (settings.includeKinds.includes('component')) {
55
+ result.components.push(...collectComponents(options));
56
+ }
57
+ if (settings.includeKinds.includes('type')) {
58
+ result.types.push(...collectTypes(options));
59
+ }
60
+ if (settings.includeKinds.includes('const')) {
61
+ result.consts.push(...collectConstants(options));
62
+ }
63
+ if (settings.includeKinds.includes('class')) {
64
+ result.classes.push(...collectClasses(options));
65
+ }
66
+ }
67
+ const summary = {
68
+ functions: result.functions.length,
69
+ components: result.components.length,
70
+ types: result.types.length,
71
+ consts: result.consts.length,
72
+ classes: result.classes.length,
73
+ };
74
+ const snapshot = JSON.stringify(result);
75
+ const tokenEstimate = estimateTokenCount(snapshot);
76
+ result.tokenEstimate = tokenEstimate;
77
+ const summaryText = `Robot extraction complete (functions=${summary.functions} components=${summary.components} types=${summary.types} consts=${summary.consts} classes=${summary.classes})`;
78
+ console.log(colors.green(summaryText));
79
+ console.log(colors.dim(`Estimated tokens: ${tokenEstimate}`));
80
+ if (exportMode === 'console') {
81
+ console.log(colors.dim('Detailed results:'));
82
+ console.log(JSON.stringify(result, null, 2));
83
+ }
84
+ else {
85
+ await exportReportLines('robot-metadata', 'json', [
86
+ summaryText,
87
+ `Token estimate: ${tokenEstimate}`,
88
+ '',
89
+ 'Detailed results:',
90
+ JSON.stringify(result, null, 2),
91
+ ]);
92
+ }
93
+ return result;
94
+ }
95
+ function estimateTokenCount(serialized) {
96
+ const length = Math.max(1, serialized.length);
97
+ return Math.ceil(length / 4);
98
+ }
@@ -0,0 +1,41 @@
1
+ import * as ts from 'typescript';
2
+ import { getDocString, getLocation, isNodeExported, withinColumnLimit, } from './shared.js';
3
+ export function collectClasses(options) {
4
+ const { sourceFile } = options.context;
5
+ const records = [];
6
+ function visit(node) {
7
+ if (!ts.isClassDeclaration(node)) {
8
+ ts.forEachChild(node, visit);
9
+ return;
10
+ }
11
+ const name = node.name?.text;
12
+ const location = getLocation(node, sourceFile);
13
+ if (!withinColumnLimit(location, options.settings.maxColumns)) {
14
+ ts.forEachChild(node, visit);
15
+ return;
16
+ }
17
+ const exported = isNodeExported(node);
18
+ if (options.settings.exportedOnly && !exported) {
19
+ ts.forEachChild(node, visit);
20
+ return;
21
+ }
22
+ const extendsClause = node.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
23
+ const implementsClause = node.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ImplementsKeyword);
24
+ const extendsText = extendsClause?.types
25
+ .map((expr) => expr.getText(sourceFile))
26
+ .join(', ');
27
+ const implementsList = implementsClause?.types.map((expr) => expr.getText(sourceFile));
28
+ records.push({
29
+ kind: 'class',
30
+ name,
31
+ location,
32
+ docString: getDocString(node),
33
+ exported,
34
+ extends: extendsText,
35
+ implements: implementsList,
36
+ });
37
+ ts.forEachChild(node, visit);
38
+ }
39
+ visit(sourceFile);
40
+ return records;
41
+ }
@@ -0,0 +1,57 @@
1
+ import * as ts from 'typescript';
2
+ import { describeParameter, getDocString, getFunctionName, getLocation, isComponentFunction, isNodeExported, withinColumnLimit, } from './shared.js';
3
+ export function collectComponents(options) {
4
+ const { sourceFile } = options.context;
5
+ const records = [];
6
+ function visit(node) {
7
+ if ((ts.isFunctionDeclaration(node) ||
8
+ ts.isFunctionExpression(node) ||
9
+ ts.isArrowFunction(node) ||
10
+ ts.isMethodDeclaration(node)) &&
11
+ Boolean(node.body)) {
12
+ const name = getFunctionName(node);
13
+ if (!isComponentFunction(node, name)) {
14
+ return;
15
+ }
16
+ const location = getLocation(node, sourceFile);
17
+ if (!withinColumnLimit(location, options.settings.maxColumns)) {
18
+ return;
19
+ }
20
+ const exported = isNodeExported(node);
21
+ if (options.settings.exportedOnly && !exported)
22
+ return;
23
+ const inputs = node.parameters.map((param) => describeParameter(param, sourceFile));
24
+ const output = node.type?.getText(sourceFile);
25
+ const jsxNode = findFirstJsx(node);
26
+ records.push({
27
+ kind: 'component',
28
+ name,
29
+ location,
30
+ docString: getDocString(node),
31
+ exported,
32
+ inputs,
33
+ output,
34
+ jsxLocation: jsxNode ? getLocation(jsxNode, sourceFile) : location,
35
+ });
36
+ }
37
+ ts.forEachChild(node, visit);
38
+ }
39
+ visit(sourceFile);
40
+ return records;
41
+ }
42
+ function findFirstJsx(node) {
43
+ let found;
44
+ function walk(child) {
45
+ if (child.kind === ts.SyntaxKind.JsxElement ||
46
+ child.kind === ts.SyntaxKind.JsxSelfClosingElement ||
47
+ child.kind === ts.SyntaxKind.JsxFragment) {
48
+ found = child;
49
+ return;
50
+ }
51
+ if (found)
52
+ return;
53
+ ts.forEachChild(child, walk);
54
+ }
55
+ ts.forEachChild(node, walk);
56
+ return found;
57
+ }
@@ -0,0 +1,42 @@
1
+ import * as ts from 'typescript';
2
+ import { getDocString, getLocation, isNodeExported, withinColumnLimit, } from './shared.js';
3
+ export function collectConstants(options) {
4
+ const { sourceFile } = options.context;
5
+ const records = [];
6
+ function visit(node) {
7
+ if (ts.isVariableStatement(node)) {
8
+ if (!(node.declarationList.flags & ts.NodeFlags.Const)) {
9
+ // only care about top-level consts
10
+ ts.forEachChild(node, visit);
11
+ return;
12
+ }
13
+ if (!ts.isSourceFile(node.parent)) {
14
+ ts.forEachChild(node, visit);
15
+ return;
16
+ }
17
+ const exported = isNodeExported(node);
18
+ if (options.settings.exportedOnly && !exported) {
19
+ ts.forEachChild(node, visit);
20
+ return;
21
+ }
22
+ node.declarationList.declarations.forEach((declaration) => {
23
+ if (!ts.isIdentifier(declaration.name))
24
+ return;
25
+ const location = getLocation(declaration.name, sourceFile);
26
+ if (!withinColumnLimit(location, options.settings.maxColumns))
27
+ return;
28
+ records.push({
29
+ kind: 'const',
30
+ name: declaration.name.text,
31
+ location,
32
+ docString: getDocString(declaration),
33
+ exported,
34
+ value: declaration.initializer?.getText(sourceFile) ?? 'undefined',
35
+ });
36
+ });
37
+ }
38
+ ts.forEachChild(node, visit);
39
+ }
40
+ visit(sourceFile);
41
+ return records;
42
+ }
@@ -0,0 +1,40 @@
1
+ import * as ts from 'typescript';
2
+ import { describeParameter, getDocString, getFunctionName, getLocation, isComponentFunction, isNodeExported, withinColumnLimit, } from './shared.js';
3
+ export function collectFunctions(options) {
4
+ const { sourceFile } = options.context;
5
+ const records = [];
6
+ function visit(node) {
7
+ if ((ts.isFunctionDeclaration(node) ||
8
+ ts.isFunctionExpression(node) ||
9
+ ts.isArrowFunction(node) ||
10
+ ts.isMethodDeclaration(node)) &&
11
+ Boolean(node.body)) {
12
+ const name = getFunctionName(node);
13
+ if (isComponentFunction(node, name)) {
14
+ // components are handled separately
15
+ return;
16
+ }
17
+ const location = getLocation(node, sourceFile);
18
+ if (!withinColumnLimit(location, options.settings.maxColumns)) {
19
+ return;
20
+ }
21
+ const exported = isNodeExported(node);
22
+ if (options.settings.exportedOnly && !exported)
23
+ return;
24
+ const inputs = node.parameters.map((param) => describeParameter(param, sourceFile));
25
+ const output = node.type?.getText(sourceFile);
26
+ records.push({
27
+ kind: 'function',
28
+ name,
29
+ location,
30
+ docString: getDocString(node),
31
+ exported,
32
+ inputs,
33
+ output,
34
+ });
35
+ }
36
+ ts.forEachChild(node, visit);
37
+ }
38
+ visit(sourceFile);
39
+ return records;
40
+ }
@@ -0,0 +1,99 @@
1
+ import * as ts from 'typescript';
2
+ export function getLocation(node, sourceFile) {
3
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
4
+ return {
5
+ file: sourceFile.fileName,
6
+ line: position.line + 1,
7
+ column: position.character + 1,
8
+ };
9
+ }
10
+ export function getDocString(node) {
11
+ const docNodes = node.jsDoc;
12
+ if (!docNodes || docNodes.length === 0)
13
+ return undefined;
14
+ const parts = [];
15
+ for (const doc of docNodes) {
16
+ if (typeof doc.comment === 'string') {
17
+ parts.push(doc.comment.trim());
18
+ }
19
+ if (doc.tags) {
20
+ for (const tag of doc.tags) {
21
+ if (typeof tag.comment === 'string') {
22
+ parts.push(tag.comment.trim());
23
+ }
24
+ }
25
+ }
26
+ }
27
+ const combined = parts.filter(Boolean).join('\n');
28
+ return combined.length ? combined : undefined;
29
+ }
30
+ export function hasExportModifier(node) {
31
+ if (!node || !('modifiers' in node))
32
+ return false;
33
+ const modifiers = node.modifiers;
34
+ if (!modifiers)
35
+ return false;
36
+ return modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
37
+ }
38
+ export function isNodeExported(node) {
39
+ if (hasExportModifier(node))
40
+ return true;
41
+ if (hasExportModifier(node.parent))
42
+ return true;
43
+ if (hasExportModifier(node.parent?.parent))
44
+ return true;
45
+ return false;
46
+ }
47
+ export function withinColumnLimit(location, maxColumns) {
48
+ if (maxColumns <= 0)
49
+ return true;
50
+ return location.column <= maxColumns;
51
+ }
52
+ export function getFunctionName(node) {
53
+ if ('name' in node && node.name && ts.isIdentifier(node.name)) {
54
+ return node.name.text;
55
+ }
56
+ const parent = node.parent;
57
+ if (!parent)
58
+ return undefined;
59
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
60
+ return parent.name.text;
61
+ }
62
+ if ((ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent)) &&
63
+ ts.isIdentifier(parent.name)) {
64
+ return parent.name.text;
65
+ }
66
+ return undefined;
67
+ }
68
+ export function isComponentFunction(node, name) {
69
+ if (!name)
70
+ return false;
71
+ if (!/^[A-Z]/.test(name))
72
+ return false;
73
+ return containsJsx(node);
74
+ }
75
+ export function containsJsx(node) {
76
+ let found = false;
77
+ function walk(child) {
78
+ if (child.kind === ts.SyntaxKind.JsxElement ||
79
+ child.kind === ts.SyntaxKind.JsxSelfClosingElement ||
80
+ child.kind === ts.SyntaxKind.JsxFragment) {
81
+ found = true;
82
+ return;
83
+ }
84
+ if (found)
85
+ return;
86
+ ts.forEachChild(child, walk);
87
+ }
88
+ ts.forEachChild(node, walk);
89
+ return found;
90
+ }
91
+ export function describeParameter(param, sourceFile) {
92
+ if (param.type) {
93
+ return param.type.getText(sourceFile);
94
+ }
95
+ if (ts.isIdentifier(param.name)) {
96
+ return param.name.text;
97
+ }
98
+ return param.name.getText(sourceFile);
99
+ }
@@ -0,0 +1,43 @@
1
+ import * as ts from 'typescript';
2
+ import { getDocString, getLocation, isNodeExported, withinColumnLimit, } from './shared.js';
3
+ export function collectTypes(options) {
4
+ const { sourceFile } = options.context;
5
+ const records = [];
6
+ function visit(node) {
7
+ let typeKind;
8
+ if (ts.isTypeAliasDeclaration(node)) {
9
+ typeKind = 'type-alias';
10
+ }
11
+ else if (ts.isInterfaceDeclaration(node)) {
12
+ typeKind = 'interface';
13
+ }
14
+ else if (ts.isEnumDeclaration(node)) {
15
+ typeKind = 'enum';
16
+ }
17
+ if (typeKind) {
18
+ const location = getLocation(node, sourceFile);
19
+ if (!withinColumnLimit(location, options.settings.maxColumns)) {
20
+ ts.forEachChild(node, visit);
21
+ return;
22
+ }
23
+ const exported = isNodeExported(node);
24
+ if (options.settings.exportedOnly && !exported) {
25
+ ts.forEachChild(node, visit);
26
+ return;
27
+ }
28
+ const name = node.name?.text;
29
+ records.push({
30
+ kind: 'type',
31
+ name,
32
+ location,
33
+ docString: getDocString(node),
34
+ exported,
35
+ typeKind,
36
+ definition: node.getText(sourceFile),
37
+ });
38
+ }
39
+ ts.forEachChild(node, visit);
40
+ }
41
+ visit(sourceFile);
42
+ return records;
43
+ }
@@ -0,0 +1 @@
1
+ export { runRobot } from './coordinator.js';
@@ -0,0 +1 @@
1
+ export const ROBOT_KINDS = ['function', 'component', 'type', 'const', 'class'];
@@ -0,0 +1,99 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { unlink, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { colors } from './log.js';
5
+ import { run } from './run.js';
6
+ import { promptSingleKey } from '../prompts.js';
7
+ const ANSI_ESCAPE = /\x1b\[[0-9;]*m/g;
8
+ const DEFAULT_EXPORT_MODE = 'console';
9
+ const EDITOR_CANDIDATES = [
10
+ process.env.MANAGER_EDITOR,
11
+ 'code',
12
+ 'code-insiders',
13
+ process.env.VISUAL,
14
+ process.env.EDITOR,
15
+ ];
16
+ export const EXPORT_MODES = ['console', 'editor'];
17
+ export async function promptExportMode(defaultMode = DEFAULT_EXPORT_MODE) {
18
+ const labels = EXPORT_MODES.map((mode) => {
19
+ const prefix = mode === 'console' ? 'c' : 'e';
20
+ const display = mode === defaultMode ? `${mode} (default)` : mode;
21
+ return `${prefix}=${display}`;
22
+ });
23
+ const question = colors.cyan(`View results in console or editor? (${labels.join(' / ')}): `);
24
+ return promptSingleKey(question, (key) => {
25
+ const normalized = key.trim().toLowerCase();
26
+ if (!normalized)
27
+ return defaultMode;
28
+ if (normalized === 'c')
29
+ return 'console';
30
+ if (normalized === 'e')
31
+ return 'editor';
32
+ return undefined;
33
+ });
34
+ }
35
+ export function captureConsoleOutput(callback) {
36
+ const lines = [];
37
+ const originalLog = console.log;
38
+ console.log = (...args) => {
39
+ lines.push(args.map((arg) => String(arg)).join(' '));
40
+ };
41
+ try {
42
+ const result = callback();
43
+ return { result, lines };
44
+ }
45
+ finally {
46
+ console.log = originalLog;
47
+ }
48
+ }
49
+ export async function exportReportLines(label, extension, lines) {
50
+ const sanitized = lines.map(stripAnsi);
51
+ const content = `${sanitized.join('\n')}\n`;
52
+ const filename = `${label}-${Date.now()}.${extension}`;
53
+ const filePath = path.join(tmpdir(), filename);
54
+ await writeFile(filePath, content, 'utf-8');
55
+ const opened = await tryOpenInEditor(filePath);
56
+ if (opened) {
57
+ await unlink(filePath).catch(() => { });
58
+ console.log(colors.green(`Opened the ${colors.bold(label)} report in your editor (temporary file removed).`));
59
+ return undefined;
60
+ }
61
+ console.log(colors.yellow(`Unable to launch an editor. The ${colors.bold(label)} report is at ${filePath}.`));
62
+ return filePath;
63
+ }
64
+ export function stripAnsi(value) {
65
+ return value.replace(ANSI_ESCAPE, '');
66
+ }
67
+ async function tryOpenInEditor(filePath) {
68
+ for (const candidate of EDITOR_CANDIDATES) {
69
+ if (!candidate)
70
+ continue;
71
+ const { command, args } = buildEditorCommand(candidate, filePath);
72
+ try {
73
+ await run(command, args, { stdio: 'ignore' });
74
+ return true;
75
+ }
76
+ catch {
77
+ // Try the next candidate if the command fails.
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+ function buildEditorCommand(raw, filePath) {
83
+ const parts = raw.trim().split(/\s+/);
84
+ if (parts.length === 0) {
85
+ throw new Error('Editor command cannot be empty.');
86
+ }
87
+ const command = parts[0];
88
+ const extraArgs = parts.slice(1);
89
+ const terminalArgs = (() => {
90
+ if (command === 'code' || command === 'code-insiders') {
91
+ return [...extraArgs, '--goto', filePath, '--wait'];
92
+ }
93
+ if (command === 'open') {
94
+ return [...extraArgs, '-W', filePath];
95
+ }
96
+ return [...extraArgs, filePath];
97
+ })();
98
+ return { command, args: terminalArgs };
99
+ }
package/dist/utils/run.js CHANGED
@@ -17,5 +17,8 @@ export function run(command, args, options = {}) {
17
17
  else
18
18
  reject(new Error(`Command "${command} ${args.join(' ')}" exited with ${code}`));
19
19
  });
20
+ child.on('error', (error) => {
21
+ reject(new Error(`Command "${command} ${args.join(' ')}" failed: ${error.message}`));
22
+ });
20
23
  });
21
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",