@solidactions/cli 1.14.0 → 1.15.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,220 @@
1
+ "use strict";
2
+ /**
3
+ * solidactions docs push <dir>
4
+ *
5
+ * Recursively uploads a local markdown tree into SA-Docs via the docs MCP
6
+ * server's `docs_vault bulk_create` tool, mirroring folder structure.
7
+ *
8
+ * Prints a report distinguishing fully-published docs from "properties pending"
9
+ * ones (docs whose frontmatter properties couldn't be validated yet).
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.docsPushWithConfig = docsPushWithConfig;
16
+ exports.docsPush = docsPush;
17
+ const fs_1 = __importDefault(require("fs"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const chalk_1 = __importDefault(require("chalk"));
20
+ const api_1 = require("../utils/api");
21
+ const mcp_1 = require("../utils/mcp");
22
+ const CHUNK_SIZE = 50;
23
+ /**
24
+ * Recursively walk `dir` and collect all *.md files.
25
+ * Returns a list of absolute paths.
26
+ */
27
+ function walkMarkdownFiles(dir) {
28
+ const results = [];
29
+ const walk = (current) => {
30
+ for (const entry of fs_1.default.readdirSync(current, { withFileTypes: true })) {
31
+ const abs = path_1.default.join(current, entry.name);
32
+ if (entry.isDirectory()) {
33
+ walk(abs);
34
+ }
35
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
36
+ results.push(abs);
37
+ }
38
+ }
39
+ };
40
+ walk(dir);
41
+ return results;
42
+ }
43
+ /**
44
+ * Convert an absolute file path to a bulk_create item, relative to `rootDir`.
45
+ */
46
+ function fileToItem(absPath, rootDir) {
47
+ const title = path_1.default.basename(absPath, '.md');
48
+ const body = fs_1.default.readFileSync(absPath, 'utf8');
49
+ const relDir = path_1.default.relative(rootDir, path_1.default.dirname(absPath));
50
+ // Use POSIX separators; omit if file is in the root dir
51
+ const relative_folder_path = relDir === '' ? undefined : relDir.split(path_1.default.sep).join('/');
52
+ const item = { title, body, _filePath: absPath };
53
+ if (relative_folder_path !== undefined) {
54
+ item.relative_folder_path = relative_folder_path;
55
+ }
56
+ return item;
57
+ }
58
+ /**
59
+ * Merge two BulkCreateSummary objects by summing all numeric fields.
60
+ */
61
+ function mergeSummaries(a, b) {
62
+ const keys = [
63
+ 'created', 'overwritten', 'skipped', 'renamed', 'errors', 'folders_created',
64
+ 'planned_create', 'planned_overwrite', 'planned_skip', 'planned_rename',
65
+ ];
66
+ const result = {};
67
+ for (const key of keys) {
68
+ const aVal = a[key] ?? 0;
69
+ const bVal = b[key] ?? 0;
70
+ if (aVal !== 0 || bVal !== 0) {
71
+ result[key] = aVal + bVal;
72
+ }
73
+ }
74
+ return result;
75
+ }
76
+ /**
77
+ * Format the merged summary into human-readable totals line.
78
+ * Normal run: created/overwritten/skipped/renamed. Dry-run: planned_*.
79
+ */
80
+ function formatSummaryLine(summary, dryRun) {
81
+ const parts = [];
82
+ if (dryRun) {
83
+ if (summary.planned_create)
84
+ parts.push(`${summary.planned_create} planned create`);
85
+ if (summary.planned_overwrite)
86
+ parts.push(`${summary.planned_overwrite} planned overwrite`);
87
+ if (summary.planned_skip)
88
+ parts.push(`${summary.planned_skip} planned skip`);
89
+ if (summary.planned_rename)
90
+ parts.push(`${summary.planned_rename} planned rename`);
91
+ }
92
+ else {
93
+ if (summary.created)
94
+ parts.push(`${summary.created} created`);
95
+ if (summary.overwritten)
96
+ parts.push(`${summary.overwritten} updated`);
97
+ if (summary.skipped)
98
+ parts.push(`${summary.skipped} skipped`);
99
+ if (summary.renamed)
100
+ parts.push(`${summary.renamed} renamed`);
101
+ if (summary.errors)
102
+ parts.push(`${summary.errors} errors`);
103
+ }
104
+ return parts.join(', ') || '0 docs';
105
+ }
106
+ /**
107
+ * Core implementation — accepts an injected config so tests can point at a
108
+ * stub server without touching the filesystem config.
109
+ */
110
+ async function docsPushWithConfig(dir, options, config) {
111
+ const absDir = path_1.default.resolve(dir);
112
+ if (!fs_1.default.existsSync(absDir) || !fs_1.default.statSync(absDir).isDirectory()) {
113
+ process.stderr.write(chalk_1.default.red(`error: "${dir}" is not a directory.\n`));
114
+ process.exit(1);
115
+ }
116
+ const allFiles = walkMarkdownFiles(absDir);
117
+ if (allFiles.length === 0) {
118
+ process.stderr.write(chalk_1.default.red(`error: no .md files found under "${dir}" — nothing to push.\n`));
119
+ process.exit(1);
120
+ }
121
+ const items = allFiles.map((f) => fileToItem(f, absDir));
122
+ // Chunk into groups of CHUNK_SIZE
123
+ const chunks = [];
124
+ for (let i = 0; i < items.length; i += CHUNK_SIZE) {
125
+ chunks.push(items.slice(i, i + CHUNK_SIZE));
126
+ }
127
+ const onConflict = options.onConflict ?? 'skip';
128
+ const allResultRows = [];
129
+ let mergedSummary = {};
130
+ for (const chunk of chunks) {
131
+ const callArgs = {
132
+ action: 'bulk_create',
133
+ on_conflict: onConflict,
134
+ items: chunk.map(({ title, body, relative_folder_path }) => {
135
+ const item = { title, body };
136
+ if (relative_folder_path !== undefined) {
137
+ item.relative_folder_path = relative_folder_path;
138
+ }
139
+ return item;
140
+ }),
141
+ };
142
+ if (options.type) {
143
+ callArgs.type = options.type;
144
+ }
145
+ if (options.dryRun) {
146
+ callArgs.dry_run = true;
147
+ }
148
+ let mcpResult;
149
+ try {
150
+ mcpResult = await (0, mcp_1.callDocsTool)(config, 'docs_vault', callArgs);
151
+ }
152
+ catch (e) {
153
+ process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
154
+ process.exit(1);
155
+ }
156
+ if (!mcpResult.ok) {
157
+ const code = mcpResult.data?.code ?? 'unknown_error';
158
+ const message = mcpResult.data?.message ?? 'MCP returned an error with no message';
159
+ process.stderr.write(chalk_1.default.red(`error: ${code}: ${message}\n`));
160
+ process.exit(1);
161
+ }
162
+ const data = mcpResult.data;
163
+ const rows = data?.results ?? [];
164
+ const summary = data?.summary ?? {};
165
+ // Correlate rows back to source files using chunk index
166
+ for (const row of rows) {
167
+ const sourceFile = chunk[row.index]?._filePath ?? '(unknown)';
168
+ allResultRows.push({
169
+ ...row,
170
+ file: path_1.default.relative(absDir, sourceFile),
171
+ });
172
+ }
173
+ mergedSummary = mergeSummaries(mergedSummary, summary);
174
+ }
175
+ // Collect pending items (results that have property_validation)
176
+ const pendingItems = allResultRows
177
+ .filter((r) => r.property_validation != null)
178
+ .map((r) => ({
179
+ file: r.file,
180
+ missing: r.property_validation.missing ?? [],
181
+ invalid: r.property_validation.invalid ?? [],
182
+ }));
183
+ // --json: output single JSON object
184
+ if (options.json) {
185
+ console.log(JSON.stringify({
186
+ summary: mergedSummary,
187
+ pending: pendingItems,
188
+ results: allResultRows,
189
+ }));
190
+ process.exit(0);
191
+ }
192
+ // Human-readable output
193
+ const summaryLine = formatSummaryLine(mergedSummary, !!options.dryRun);
194
+ if (options.dryRun) {
195
+ console.log(chalk_1.default.cyan(`[dry-run preview] ${summaryLine}`));
196
+ }
197
+ else {
198
+ console.log(chalk_1.default.green(`done: ${summaryLine}`));
199
+ }
200
+ if (pendingItems.length > 0) {
201
+ console.log(chalk_1.default.yellow(`\nProperties pending (${pendingItems.length} doc${pendingItems.length === 1 ? '' : 's'}):`));
202
+ for (const item of pendingItems) {
203
+ console.log(chalk_1.default.yellow(` ${item.file}`));
204
+ if (item.missing.length > 0) {
205
+ console.log(chalk_1.default.yellow(` missing: ${item.missing.join(', ')}`));
206
+ }
207
+ for (const inv of item.invalid) {
208
+ console.log(chalk_1.default.yellow(` invalid: ${inv.key} — ${inv.reason}`));
209
+ }
210
+ }
211
+ }
212
+ process.exit(0);
213
+ }
214
+ /**
215
+ * Entry point called from index.ts.
216
+ */
217
+ async function docsPush(dir, options) {
218
+ const config = await (0, api_1.requireConfigWithWorkspace)();
219
+ await docsPushWithConfig(dir, options, config);
220
+ }
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ const skill_list_1 = require("./commands/skill-list");
38
38
  const skill_view_1 = require("./commands/skill-view");
39
39
  const skill_delete_1 = require("./commands/skill-delete");
40
40
  const role_push_1 = require("./commands/role-push");
41
+ const docs_push_1 = require("./commands/docs-push");
41
42
  const config_1 = require("./utils/config");
42
43
  // eslint-disable-next-line @typescript-eslint/no-var-requires
43
44
  const pkg = require('../package.json');
@@ -454,6 +455,21 @@ role
454
455
  .action(async (dir, options) => {
455
456
  await (0, role_push_1.rolePush)(dir, options);
456
457
  });
458
+ // =============================================================================
459
+ // docs <subcommand> (SA-Docs surface)
460
+ // =============================================================================
461
+ const docs = program.command('docs').description('Manage docs in SA-Docs');
462
+ docs
463
+ .command('push')
464
+ .description('Recursively upload a local markdown tree into SA-Docs')
465
+ .argument('<dir>', 'Path to the directory containing markdown files')
466
+ .option('--on-conflict <mode>', 'Conflict resolution: skip|overwrite|rename (default: skip)', 'skip')
467
+ .option('--type <slug>', 'Doc-type slug to apply to all uploaded docs')
468
+ .option('--dry-run', 'Preview what would be created without writing')
469
+ .option('--json', 'Output result as JSON')
470
+ .action(async (dir, options) => {
471
+ await (0, docs_push_1.docsPush)(dir, { onConflict: options.onConflict, type: options.type, dryRun: options.dryRun, json: options.json });
472
+ });
457
473
  program.parseAsync().catch((err) => {
458
474
  console.error(chalk_1.default.red(err.message ?? String(err)));
459
475
  process.exit(1);
package/dist/utils/mcp.js CHANGED
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  /**
3
- * Minimal MCP client for the SolidActions crews MCP server.
3
+ * Minimal MCP client for the SolidActions in-app MCP servers.
4
4
  *
5
5
  * Sends a single stateless JSON-RPC tools/call POST. No initialize handshake
6
- * required — the streamable-HTTP transport at /mcp/crews accepts single
7
- * stateless POSTs (confirmed live).
6
+ * required — the streamable-HTTP transport accepts single stateless POSTs
7
+ * (confirmed live).
8
8
  */
9
9
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
10
  if (k2 === undefined) k2 = k;
@@ -41,16 +41,17 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  })();
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.callCrewsTool = callCrewsTool;
44
+ exports.callDocsTool = callDocsTool;
44
45
  const http = __importStar(require("http"));
45
46
  const https = __importStar(require("https"));
46
47
  const url_1 = require("url");
47
48
  const api_1 = require("./api");
48
49
  /**
49
- * Call a single MCP tool on /mcp/crews.
50
+ * Internal: call a single MCP tool on the given endpoint path.
50
51
  *
51
52
  * Returns { ok: true, data: <success shape> } or { ok: false, data: { code, message } }.
52
53
  */
53
- async function callCrewsTool(config, toolName, args) {
54
+ async function callMcpTool(config, endpointPath, toolName, args) {
54
55
  const baseHeaders = (0, api_1.getApiHeaders)(config, 'application/json');
55
56
  // Override Accept to include text/event-stream for streamable-HTTP transport
56
57
  const headers = {
@@ -66,7 +67,7 @@ async function callCrewsTool(config, toolName, args) {
66
67
  arguments: args,
67
68
  },
68
69
  });
69
- const parsed = new url_1.URL(`${config.host}/mcp/crews`);
70
+ const parsed = new url_1.URL(`${config.host}${endpointPath}`);
70
71
  const isHttps = parsed.protocol === 'https:';
71
72
  const transport = isHttps ? https : http;
72
73
  const responseData = await new Promise((resolve, reject) => {
@@ -115,3 +116,15 @@ async function callCrewsTool(config, toolName, args) {
115
116
  }
116
117
  return { ok: !isError, data: toolData };
117
118
  }
119
+ /**
120
+ * Call a single MCP tool on /mcp/crews.
121
+ */
122
+ async function callCrewsTool(config, toolName, args) {
123
+ return callMcpTool(config, '/mcp/crews', toolName, args);
124
+ }
125
+ /**
126
+ * Call a single MCP tool on /mcp/docs.
127
+ */
128
+ async function callDocsTool(config, toolName, args) {
129
+ return callMcpTool(config, '/mcp/docs', toolName, args);
130
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidactions/cli",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "SolidActions CLI - Deploy and manage workflow automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {