@solidactions/cli 1.14.0 → 1.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/dist/commands/docs-push.js +224 -0
- package/dist/index.js +17 -0
- package/dist/utils/mcp.js +19 -6
- package/package.json +1 -1
|
@@ -0,0 +1,224 @@
|
|
|
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.folder) {
|
|
146
|
+
// Top-level base for every item; each item's relative_folder_path nests under it.
|
|
147
|
+
callArgs.folder_path = options.folder;
|
|
148
|
+
}
|
|
149
|
+
if (options.dryRun) {
|
|
150
|
+
callArgs.dry_run = true;
|
|
151
|
+
}
|
|
152
|
+
let mcpResult;
|
|
153
|
+
try {
|
|
154
|
+
mcpResult = await (0, mcp_1.callDocsTool)(config, 'docs_vault', callArgs);
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
process.stderr.write(chalk_1.default.red(`error: MCP request failed — ${e.message}\n`));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
if (!mcpResult.ok) {
|
|
161
|
+
const code = mcpResult.data?.code ?? 'unknown_error';
|
|
162
|
+
const message = mcpResult.data?.message ?? 'MCP returned an error with no message';
|
|
163
|
+
process.stderr.write(chalk_1.default.red(`error: ${code}: ${message}\n`));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
const data = mcpResult.data;
|
|
167
|
+
const rows = data?.results ?? [];
|
|
168
|
+
const summary = data?.summary ?? {};
|
|
169
|
+
// Correlate rows back to source files using chunk index
|
|
170
|
+
for (const row of rows) {
|
|
171
|
+
const sourceFile = chunk[row.index]?._filePath ?? '(unknown)';
|
|
172
|
+
allResultRows.push({
|
|
173
|
+
...row,
|
|
174
|
+
file: path_1.default.relative(absDir, sourceFile),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
mergedSummary = mergeSummaries(mergedSummary, summary);
|
|
178
|
+
}
|
|
179
|
+
// Collect pending items (results that have property_validation)
|
|
180
|
+
const pendingItems = allResultRows
|
|
181
|
+
.filter((r) => r.property_validation != null)
|
|
182
|
+
.map((r) => ({
|
|
183
|
+
file: r.file,
|
|
184
|
+
missing: r.property_validation.missing ?? [],
|
|
185
|
+
invalid: r.property_validation.invalid ?? [],
|
|
186
|
+
}));
|
|
187
|
+
// --json: output single JSON object
|
|
188
|
+
if (options.json) {
|
|
189
|
+
console.log(JSON.stringify({
|
|
190
|
+
summary: mergedSummary,
|
|
191
|
+
pending: pendingItems,
|
|
192
|
+
results: allResultRows,
|
|
193
|
+
}));
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
// Human-readable output
|
|
197
|
+
const summaryLine = formatSummaryLine(mergedSummary, !!options.dryRun);
|
|
198
|
+
if (options.dryRun) {
|
|
199
|
+
console.log(chalk_1.default.cyan(`[dry-run preview] ${summaryLine}`));
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.log(chalk_1.default.green(`done: ${summaryLine}`));
|
|
203
|
+
}
|
|
204
|
+
if (pendingItems.length > 0) {
|
|
205
|
+
console.log(chalk_1.default.yellow(`\nProperties pending (${pendingItems.length} doc${pendingItems.length === 1 ? '' : 's'}):`));
|
|
206
|
+
for (const item of pendingItems) {
|
|
207
|
+
console.log(chalk_1.default.yellow(` ${item.file}`));
|
|
208
|
+
if (item.missing.length > 0) {
|
|
209
|
+
console.log(chalk_1.default.yellow(` missing: ${item.missing.join(', ')}`));
|
|
210
|
+
}
|
|
211
|
+
for (const inv of item.invalid) {
|
|
212
|
+
console.log(chalk_1.default.yellow(` invalid: ${inv.key} — ${inv.reason}`));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Entry point called from index.ts.
|
|
220
|
+
*/
|
|
221
|
+
async function docsPush(dir, options) {
|
|
222
|
+
const config = await (0, api_1.requireConfigWithWorkspace)();
|
|
223
|
+
await docsPushWithConfig(dir, options, config);
|
|
224
|
+
}
|
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,22 @@ 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('--folder <base>', 'Nest the whole upload under this base folder path in SA-Docs')
|
|
469
|
+
.option('--dry-run', 'Preview what would be created without writing')
|
|
470
|
+
.option('--json', 'Output result as JSON')
|
|
471
|
+
.action(async (dir, options) => {
|
|
472
|
+
await (0, docs_push_1.docsPush)(dir, { onConflict: options.onConflict, type: options.type, folder: options.folder, dryRun: options.dryRun, json: options.json });
|
|
473
|
+
});
|
|
457
474
|
program.parseAsync().catch((err) => {
|
|
458
475
|
console.error(chalk_1.default.red(err.message ?? String(err)));
|
|
459
476
|
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
|
|
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
|
|
7
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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}
|
|
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
|
+
}
|