rowbound 1.0.2
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/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/adapters/adapter.d.ts +1 -0
- package/dist/adapters/adapter.js +1 -0
- package/dist/adapters/sheets/sheets-adapter.d.ts +66 -0
- package/dist/adapters/sheets/sheets-adapter.js +531 -0
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.js +397 -0
- package/dist/cli/env.d.ts +3 -0
- package/dist/cli/env.js +103 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +6 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +39 -0
- package/dist/cli/init.d.ts +10 -0
- package/dist/cli/init.js +72 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +212 -0
- package/dist/cli/runs.d.ts +2 -0
- package/dist/cli/runs.js +108 -0
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +108 -0
- package/dist/cli/sync.d.ts +2 -0
- package/dist/cli/sync.js +84 -0
- package/dist/cli/watch.d.ts +2 -0
- package/dist/cli/watch.js +348 -0
- package/dist/core/condition.d.ts +25 -0
- package/dist/core/condition.js +66 -0
- package/dist/core/defaults.d.ts +3 -0
- package/dist/core/defaults.js +7 -0
- package/dist/core/engine.d.ts +50 -0
- package/dist/core/engine.js +234 -0
- package/dist/core/env.d.ts +13 -0
- package/dist/core/env.js +72 -0
- package/dist/core/exec.d.ts +24 -0
- package/dist/core/exec.js +134 -0
- package/dist/core/extractor.d.ts +10 -0
- package/dist/core/extractor.js +33 -0
- package/dist/core/http-client.d.ts +32 -0
- package/dist/core/http-client.js +161 -0
- package/dist/core/rate-limiter.d.ts +25 -0
- package/dist/core/rate-limiter.js +64 -0
- package/dist/core/reconcile.d.ts +24 -0
- package/dist/core/reconcile.js +192 -0
- package/dist/core/run-format.d.ts +39 -0
- package/dist/core/run-format.js +201 -0
- package/dist/core/run-state.d.ts +64 -0
- package/dist/core/run-state.js +141 -0
- package/dist/core/run-tracker.d.ts +15 -0
- package/dist/core/run-tracker.js +57 -0
- package/dist/core/safe-compare.d.ts +8 -0
- package/dist/core/safe-compare.js +19 -0
- package/dist/core/shell-escape.d.ts +7 -0
- package/dist/core/shell-escape.js +9 -0
- package/dist/core/tab-resolver.d.ts +17 -0
- package/dist/core/tab-resolver.js +44 -0
- package/dist/core/template.d.ts +32 -0
- package/dist/core/template.js +82 -0
- package/dist/core/types.d.ts +105 -0
- package/dist/core/types.js +2 -0
- package/dist/core/url-guard.d.ts +21 -0
- package/dist/core/url-guard.js +184 -0
- package/dist/core/validator.d.ts +11 -0
- package/dist/core/validator.js +261 -0
- package/dist/core/waterfall.d.ts +26 -0
- package/dist/core/waterfall.js +55 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +943 -0
- package/package.json +67 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { SheetsAdapter } from "../adapters/sheets/sheets-adapter.js";
|
|
2
|
+
import { getTabConfig, resolveTabGid } from "../core/tab-resolver.js";
|
|
3
|
+
import { validateConfig } from "../core/validator.js";
|
|
4
|
+
import { error, warn } from "./format.js";
|
|
5
|
+
export function registerConfig(program) {
|
|
6
|
+
const configCmd = program
|
|
7
|
+
.command("config")
|
|
8
|
+
.description("Manage pipeline configuration")
|
|
9
|
+
.action(() => {
|
|
10
|
+
configCmd.help();
|
|
11
|
+
});
|
|
12
|
+
// rowbound config show <sheetId> [--tab <name>]
|
|
13
|
+
configCmd
|
|
14
|
+
.command("show")
|
|
15
|
+
.description("Display pipeline config as formatted JSON")
|
|
16
|
+
.argument("<sheetId>", "Google Sheets spreadsheet ID")
|
|
17
|
+
.option("--tab <name>", "Sheet tab name")
|
|
18
|
+
.action(async (sheetId, opts) => {
|
|
19
|
+
const adapter = new SheetsAdapter();
|
|
20
|
+
const ref = { spreadsheetId: sheetId, sheetName: opts.tab || "Sheet1" };
|
|
21
|
+
try {
|
|
22
|
+
const existing = await adapter.readConfig(ref);
|
|
23
|
+
if (!existing) {
|
|
24
|
+
console.error(error("No Rowbound config found for this sheet."));
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (opts.tab && !existing.tabs) {
|
|
29
|
+
console.warn(warn("Note: Config is v1 format (single-tab). Run 'rowbound sync' to migrate to v2."));
|
|
30
|
+
}
|
|
31
|
+
if (opts.tab && existing.tabs) {
|
|
32
|
+
// Show just the specified tab's config
|
|
33
|
+
const resolved = resolveTabGid(existing, opts.tab);
|
|
34
|
+
if (!resolved) {
|
|
35
|
+
console.error(error(`Tab "${opts.tab}" not found. Available: ${Object.values(existing.tabs)
|
|
36
|
+
.map((t) => t.name)
|
|
37
|
+
.join(", ")}`));
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(JSON.stringify({ gid: resolved.gid, ...resolved.tab }, null, 2));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.log(JSON.stringify(existing, null, 2));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
49
|
+
console.error(error("Failed to read config:"), msg);
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// rowbound config add-action <sheetId> --json '<action JSON>' [--tab <name>]
|
|
54
|
+
configCmd
|
|
55
|
+
.command("add-action")
|
|
56
|
+
.description("Add an action to the pipeline config")
|
|
57
|
+
.argument("<sheetId>", "Google Sheets spreadsheet ID")
|
|
58
|
+
.requiredOption("--json <actionJson>", "Action definition as JSON string")
|
|
59
|
+
.option("--tab <name>", "Sheet tab name")
|
|
60
|
+
.action(async (sheetId, opts) => {
|
|
61
|
+
const adapter = new SheetsAdapter();
|
|
62
|
+
const ref = { spreadsheetId: sheetId, sheetName: opts.tab || "Sheet1" };
|
|
63
|
+
try {
|
|
64
|
+
let action;
|
|
65
|
+
try {
|
|
66
|
+
action = JSON.parse(opts.json);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
console.error(error("Invalid JSON for action definition."));
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!action.id || !action.type || !action.target) {
|
|
74
|
+
console.error(error("Action must have at least 'id', 'type', and 'target' fields."));
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const existing = await adapter.readConfig(ref);
|
|
79
|
+
if (!existing) {
|
|
80
|
+
console.error(error("No Rowbound config found. Run 'rowbound init <sheetId>' first."));
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (existing.tabs) {
|
|
85
|
+
// v2: add to the specific tab
|
|
86
|
+
const { gid, tab } = getTabConfig(existing, opts.tab);
|
|
87
|
+
if (tab.actions.some((s) => s.id === action.id)) {
|
|
88
|
+
console.error(error(`Action with id "${action.id}" already exists.`));
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
tab.actions.push(action);
|
|
93
|
+
existing.tabs[gid] = tab;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// v1 fallback
|
|
97
|
+
if (existing.actions.some((s) => s.id === action.id)) {
|
|
98
|
+
console.error(error(`Action with id "${action.id}" already exists.`));
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
existing.actions.push(action);
|
|
103
|
+
}
|
|
104
|
+
await adapter.writeConfig(ref, existing);
|
|
105
|
+
console.log(`Added action "${action.id}" (${action.type} -> ${action.target})`);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
109
|
+
console.error(error("Failed to add action:"), msg);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// rowbound config remove-action <sheetId> --action <id> [--tab <name>]
|
|
114
|
+
configCmd
|
|
115
|
+
.command("remove-action")
|
|
116
|
+
.description("Remove an action from the pipeline config")
|
|
117
|
+
.argument("<sheetId>", "Google Sheets spreadsheet ID")
|
|
118
|
+
.requiredOption("--action <id>", "Action ID to remove")
|
|
119
|
+
.option("--tab <name>", "Sheet tab name")
|
|
120
|
+
.action(async (sheetId, opts) => {
|
|
121
|
+
const adapter = new SheetsAdapter();
|
|
122
|
+
const ref = { spreadsheetId: sheetId, sheetName: opts.tab || "Sheet1" };
|
|
123
|
+
try {
|
|
124
|
+
const existing = await adapter.readConfig(ref);
|
|
125
|
+
if (!existing) {
|
|
126
|
+
console.error(error("No Rowbound config found. Run 'rowbound init <sheetId>' first."));
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (existing.tabs) {
|
|
131
|
+
const { gid, tab } = getTabConfig(existing, opts.tab);
|
|
132
|
+
const originalLength = tab.actions.length;
|
|
133
|
+
tab.actions = tab.actions.filter((s) => s.id !== opts.action);
|
|
134
|
+
if (tab.actions.length === originalLength) {
|
|
135
|
+
console.error(error(`Action "${opts.action}" not found in config.`));
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
existing.tabs[gid] = tab;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const originalLength = existing.actions.length;
|
|
143
|
+
existing.actions = existing.actions.filter((s) => s.id !== opts.action);
|
|
144
|
+
if (existing.actions.length === originalLength) {
|
|
145
|
+
console.error(error(`Action "${opts.action}" not found in config.`));
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
await adapter.writeConfig(ref, existing);
|
|
151
|
+
console.log(`Removed action "${opts.action}".`);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
+
console.error(error("Failed to remove action:"), msg);
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
// rowbound config update-action <sheetId> --action <id> --json '<partial JSON>' [--tab <name>]
|
|
160
|
+
configCmd
|
|
161
|
+
.command("update-action")
|
|
162
|
+
.description("Update an action in the pipeline config (merge partial JSON)")
|
|
163
|
+
.argument("<sheetId>", "Google Sheets spreadsheet ID")
|
|
164
|
+
.requiredOption("--action <id>", "Action ID to update")
|
|
165
|
+
.requiredOption("--json <partialJson>", "Partial action definition to merge")
|
|
166
|
+
.option("--tab <name>", "Sheet tab name")
|
|
167
|
+
.action(async (sheetId, opts) => {
|
|
168
|
+
const adapter = new SheetsAdapter();
|
|
169
|
+
const ref = { spreadsheetId: sheetId, sheetName: opts.tab || "Sheet1" };
|
|
170
|
+
try {
|
|
171
|
+
let patch;
|
|
172
|
+
try {
|
|
173
|
+
patch = JSON.parse(opts.json);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
console.error(error("Invalid JSON for action update."));
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const existing = await adapter.readConfig(ref);
|
|
181
|
+
if (!existing) {
|
|
182
|
+
console.error(error("No Rowbound config found. Run 'rowbound init <sheetId>' first."));
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let actions;
|
|
187
|
+
let gid;
|
|
188
|
+
if (existing.tabs) {
|
|
189
|
+
const resolved = getTabConfig(existing, opts.tab);
|
|
190
|
+
gid = resolved.gid;
|
|
191
|
+
actions = resolved.tab.actions;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
actions = existing.actions;
|
|
195
|
+
}
|
|
196
|
+
const actionIndex = actions.findIndex((s) => s.id === opts.action);
|
|
197
|
+
if (actionIndex === -1) {
|
|
198
|
+
console.error(error(`Action "${opts.action}" not found in config.`));
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
// If renaming the ID, check for duplicates
|
|
203
|
+
if (patch.id && patch.id !== opts.action) {
|
|
204
|
+
if (actions.some((s) => s.id === patch.id)) {
|
|
205
|
+
console.error(error(`Action with id "${patch.id}" already exists.`));
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
actions[actionIndex] = {
|
|
211
|
+
...actions[actionIndex],
|
|
212
|
+
...patch,
|
|
213
|
+
};
|
|
214
|
+
if (existing.tabs && gid) {
|
|
215
|
+
existing.tabs[gid].actions = actions;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
existing.actions = actions;
|
|
219
|
+
}
|
|
220
|
+
await adapter.writeConfig(ref, existing);
|
|
221
|
+
console.log(`Updated action "${opts.action}"${patch.id && patch.id !== opts.action ? ` → "${patch.id}"` : ""}`);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
225
|
+
console.error(error("Failed to update action:"), msg);
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// rowbound config set <sheetId> [--concurrency <n>] [--rate-limit <n>] [--retry-attempts <n>] [--retry-backoff <strategy>] [--tab <name>]
|
|
230
|
+
configCmd
|
|
231
|
+
.command("set")
|
|
232
|
+
.description("Update pipeline settings")
|
|
233
|
+
.argument("<sheetId>", "Google Sheets spreadsheet ID")
|
|
234
|
+
.option("--concurrency <n>", "Max concurrent rows")
|
|
235
|
+
.option("--rate-limit <n>", "Max requests per second")
|
|
236
|
+
.option("--retry-attempts <n>", "Number of retry attempts")
|
|
237
|
+
.option("--retry-backoff <strategy>", "Backoff strategy (exponential, linear, fixed)")
|
|
238
|
+
.option("--tab <name>", "Sheet tab name")
|
|
239
|
+
.action(async (sheetId, opts) => {
|
|
240
|
+
const adapter = new SheetsAdapter();
|
|
241
|
+
const ref = { spreadsheetId: sheetId, sheetName: opts.tab || "Sheet1" };
|
|
242
|
+
try {
|
|
243
|
+
const existing = await adapter.readConfig(ref);
|
|
244
|
+
if (!existing) {
|
|
245
|
+
console.error(error("No Rowbound config found. Run 'rowbound init <sheetId>' first."));
|
|
246
|
+
process.exitCode = 1;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const changes = [];
|
|
250
|
+
if (opts.concurrency !== undefined) {
|
|
251
|
+
const val = parseInt(opts.concurrency, 10);
|
|
252
|
+
if (Number.isNaN(val) || val <= 0) {
|
|
253
|
+
console.error(error("--concurrency must be a positive integer."));
|
|
254
|
+
process.exitCode = 1;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
existing.settings.concurrency = val;
|
|
258
|
+
changes.push(`concurrency=${val}`);
|
|
259
|
+
}
|
|
260
|
+
if (opts.rateLimit !== undefined) {
|
|
261
|
+
const val = parseInt(opts.rateLimit, 10);
|
|
262
|
+
if (Number.isNaN(val) || val <= 0) {
|
|
263
|
+
console.error(error("--rate-limit must be a positive integer."));
|
|
264
|
+
process.exitCode = 1;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
existing.settings.rateLimit = val;
|
|
268
|
+
changes.push(`rateLimit=${val}`);
|
|
269
|
+
}
|
|
270
|
+
if (opts.retryAttempts !== undefined) {
|
|
271
|
+
const val = parseInt(opts.retryAttempts, 10);
|
|
272
|
+
if (Number.isNaN(val) || val < 0) {
|
|
273
|
+
console.error(error("--retry-attempts must be a non-negative integer."));
|
|
274
|
+
process.exitCode = 1;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
existing.settings.retryAttempts = val;
|
|
278
|
+
changes.push(`retryAttempts=${val}`);
|
|
279
|
+
}
|
|
280
|
+
if (opts.retryBackoff !== undefined) {
|
|
281
|
+
const validStrategies = ["exponential", "linear", "fixed"];
|
|
282
|
+
if (!validStrategies.includes(opts.retryBackoff)) {
|
|
283
|
+
console.error(error(`Invalid --retry-backoff value "${opts.retryBackoff}". Must be one of: ${validStrategies.join(", ")}`));
|
|
284
|
+
process.exitCode = 1;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
existing.settings.retryBackoff = opts.retryBackoff;
|
|
288
|
+
changes.push(`retryBackoff=${opts.retryBackoff}`);
|
|
289
|
+
}
|
|
290
|
+
if (changes.length === 0) {
|
|
291
|
+
console.error(error("No settings specified. Use --concurrency, --rate-limit, --retry-attempts, or --retry-backoff."));
|
|
292
|
+
process.exitCode = 1;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
await adapter.writeConfig(ref, existing);
|
|
296
|
+
console.log(`Updated settings: ${changes.join(", ")}`);
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
300
|
+
console.error(error("Failed to update settings:"), msg);
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
// rowbound config validate <sheetId> [--tab <name>] [--json]
|
|
305
|
+
configCmd
|
|
306
|
+
.command("validate")
|
|
307
|
+
.description("Validate the pipeline config")
|
|
308
|
+
.argument("<sheetId>", "Google Sheets spreadsheet ID")
|
|
309
|
+
.option("--tab <name>", "Sheet tab name")
|
|
310
|
+
.option("--json", "Output validation result as JSON")
|
|
311
|
+
.action(async (sheetId, opts) => {
|
|
312
|
+
const adapter = new SheetsAdapter();
|
|
313
|
+
const ref = { spreadsheetId: sheetId, sheetName: opts.tab || "Sheet1" };
|
|
314
|
+
try {
|
|
315
|
+
const existing = await adapter.readConfig(ref);
|
|
316
|
+
if (!existing) {
|
|
317
|
+
if (opts.json) {
|
|
318
|
+
console.log(JSON.stringify({
|
|
319
|
+
valid: false,
|
|
320
|
+
errors: ["No Rowbound config found for this sheet."],
|
|
321
|
+
warnings: [],
|
|
322
|
+
}, null, 2));
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
console.error(error("No Rowbound config found for this sheet."));
|
|
326
|
+
}
|
|
327
|
+
process.exitCode = 1;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// For v2, validate per-tab
|
|
331
|
+
let result;
|
|
332
|
+
let tabName;
|
|
333
|
+
let actionCount;
|
|
334
|
+
if (existing.tabs) {
|
|
335
|
+
const { tab } = getTabConfig(existing, opts.tab);
|
|
336
|
+
tabName = tab.name;
|
|
337
|
+
actionCount = tab.actions.length;
|
|
338
|
+
const tabValidationConfig = {
|
|
339
|
+
...existing,
|
|
340
|
+
actions: tab.actions,
|
|
341
|
+
};
|
|
342
|
+
result = validateConfig(tabValidationConfig);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
actionCount = existing.actions.length;
|
|
346
|
+
result = validateConfig(existing);
|
|
347
|
+
}
|
|
348
|
+
if (opts.json) {
|
|
349
|
+
console.log(JSON.stringify(result, null, 2));
|
|
350
|
+
if (!result.valid) {
|
|
351
|
+
process.exitCode = 1;
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (result.valid) {
|
|
356
|
+
if (result.warnings.length > 0) {
|
|
357
|
+
console.log(warn(`Config is valid with ${result.warnings.length} warning(s):`));
|
|
358
|
+
for (const w of result.warnings) {
|
|
359
|
+
console.warn(warn(` - ${w}`));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
console.log("Config is valid.");
|
|
364
|
+
}
|
|
365
|
+
console.log(` Version: ${existing.version}`);
|
|
366
|
+
if (tabName) {
|
|
367
|
+
console.log(` Tab: ${tabName}`);
|
|
368
|
+
}
|
|
369
|
+
console.log(` Actions: ${actionCount}`);
|
|
370
|
+
console.log(` Settings: concurrency=${existing.settings.concurrency}, rateLimit=${existing.settings.rateLimit}/s`);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
console.error(error("Config validation failed:"));
|
|
374
|
+
for (const e of result.errors) {
|
|
375
|
+
console.error(error(` - ${e}`));
|
|
376
|
+
}
|
|
377
|
+
process.exitCode = 1;
|
|
378
|
+
if (result.warnings.length > 0) {
|
|
379
|
+
console.warn(warn("Warnings:"));
|
|
380
|
+
for (const w of result.warnings) {
|
|
381
|
+
console.warn(warn(` - ${w}`));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
388
|
+
if (opts.json) {
|
|
389
|
+
console.log(JSON.stringify({ valid: false, errors: [msg], warnings: [] }, null, 2));
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
console.error(error("Failed to validate config:"), msg);
|
|
393
|
+
}
|
|
394
|
+
process.exitCode = 1;
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
package/dist/cli/env.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
const CONFIG_DIR = join(homedir(), ".config", "rowbound");
|
|
6
|
+
const ENV_FILE = join(CONFIG_DIR, ".env");
|
|
7
|
+
export function getGlobalEnvPath() {
|
|
8
|
+
return ENV_FILE;
|
|
9
|
+
}
|
|
10
|
+
function ensureConfigDir() {
|
|
11
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
12
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function readEnvFile() {
|
|
16
|
+
const entries = new Map();
|
|
17
|
+
if (!existsSync(ENV_FILE))
|
|
18
|
+
return entries;
|
|
19
|
+
const content = readFileSync(ENV_FILE, "utf-8");
|
|
20
|
+
for (const line of content.split("\n")) {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
23
|
+
continue;
|
|
24
|
+
const eqIndex = trimmed.indexOf("=");
|
|
25
|
+
if (eqIndex === -1)
|
|
26
|
+
continue;
|
|
27
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
28
|
+
const value = trimmed.slice(eqIndex + 1).trim();
|
|
29
|
+
entries.set(key, value);
|
|
30
|
+
}
|
|
31
|
+
return entries;
|
|
32
|
+
}
|
|
33
|
+
function writeEnvFile(entries) {
|
|
34
|
+
ensureConfigDir();
|
|
35
|
+
const lines = Array.from(entries.entries()).map(([key, value]) => `${key}=${value}`);
|
|
36
|
+
writeFileSync(ENV_FILE, `${lines.join("\n")}\n`, { mode: 0o600 });
|
|
37
|
+
}
|
|
38
|
+
function maskValue(value) {
|
|
39
|
+
if (value.length <= 8)
|
|
40
|
+
return "****";
|
|
41
|
+
return `${value.slice(0, 4)}****${value.slice(-4)}`;
|
|
42
|
+
}
|
|
43
|
+
export function registerEnv(program) {
|
|
44
|
+
const env = program
|
|
45
|
+
.command("env")
|
|
46
|
+
.description("Manage API keys stored in ~/.config/rowbound/.env");
|
|
47
|
+
env
|
|
48
|
+
.command("set <key=value>")
|
|
49
|
+
.description("Set an environment variable")
|
|
50
|
+
.action((pair) => {
|
|
51
|
+
const eqIndex = pair.indexOf("=");
|
|
52
|
+
if (eqIndex === -1) {
|
|
53
|
+
console.error(pc.red("Invalid format. Use: rowbound env set KEY=value"));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const key = pair.slice(0, eqIndex).trim();
|
|
57
|
+
const value = pair.slice(eqIndex + 1).trim();
|
|
58
|
+
if (!key) {
|
|
59
|
+
console.error(pc.red("Key cannot be empty."));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const entries = readEnvFile();
|
|
63
|
+
const isUpdate = entries.has(key);
|
|
64
|
+
entries.set(key, value);
|
|
65
|
+
writeEnvFile(entries);
|
|
66
|
+
console.log(isUpdate
|
|
67
|
+
? `${pc.green("Updated")} ${pc.bold(key)}`
|
|
68
|
+
: `${pc.green("Set")} ${pc.bold(key)}`);
|
|
69
|
+
});
|
|
70
|
+
env
|
|
71
|
+
.command("remove <key>")
|
|
72
|
+
.description("Remove an environment variable")
|
|
73
|
+
.action((key) => {
|
|
74
|
+
const entries = readEnvFile();
|
|
75
|
+
if (!entries.has(key)) {
|
|
76
|
+
console.error(pc.yellow(`${key} not found.`));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
entries.delete(key);
|
|
80
|
+
writeEnvFile(entries);
|
|
81
|
+
console.log(`${pc.green("Removed")} ${pc.bold(key)}`);
|
|
82
|
+
});
|
|
83
|
+
env
|
|
84
|
+
.command("list")
|
|
85
|
+
.description("List all stored environment variables (values masked)")
|
|
86
|
+
.action(() => {
|
|
87
|
+
const entries = readEnvFile();
|
|
88
|
+
if (entries.size === 0) {
|
|
89
|
+
console.log(pc.dim("No environment variables configured."));
|
|
90
|
+
console.log(pc.dim(`Run: rowbound env set API_KEY=your_key`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const [key, value] of entries) {
|
|
94
|
+
console.log(`${pc.bold(key)}=${pc.dim(maskValue(value))}`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
env
|
|
98
|
+
.command("path")
|
|
99
|
+
.description("Print the path to the global env file")
|
|
100
|
+
.action(() => {
|
|
101
|
+
console.log(ENV_FILE);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import dotenv from "dotenv";
|
|
5
|
+
import { registerConfig } from "./config.js";
|
|
6
|
+
import { getGlobalEnvPath, registerEnv } from "./env.js";
|
|
7
|
+
import { registerInit } from "./init.js";
|
|
8
|
+
import { registerRun } from "./run.js";
|
|
9
|
+
import { registerRuns } from "./runs.js";
|
|
10
|
+
import { registerStatus } from "./status.js";
|
|
11
|
+
import { registerSync } from "./sync.js";
|
|
12
|
+
import { registerWatch } from "./watch.js";
|
|
13
|
+
// Load local .env first (higher priority), then global ~/.config/rowbound/.env
|
|
14
|
+
// dotenv never overwrites existing process.env, so shell vars always win
|
|
15
|
+
dotenv.config();
|
|
16
|
+
dotenv.config({ path: getGlobalEnvPath() });
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const pkg = require("../../package.json");
|
|
19
|
+
const program = new Command();
|
|
20
|
+
program
|
|
21
|
+
.name("rowbound")
|
|
22
|
+
.description("Open-source CLI for Google Sheets enrichment — waterfalls, conditions, HTTP API integrations")
|
|
23
|
+
.version(pkg.version);
|
|
24
|
+
registerInit(program);
|
|
25
|
+
registerRun(program);
|
|
26
|
+
registerConfig(program);
|
|
27
|
+
registerStatus(program);
|
|
28
|
+
registerWatch(program);
|
|
29
|
+
registerRuns(program);
|
|
30
|
+
registerSync(program);
|
|
31
|
+
registerEnv(program);
|
|
32
|
+
program
|
|
33
|
+
.command("mcp")
|
|
34
|
+
.description("Start MCP server (stdio)")
|
|
35
|
+
.action(async () => {
|
|
36
|
+
const { startMcpServer } = await import("../mcp/server.js");
|
|
37
|
+
await startMcpServer();
|
|
38
|
+
});
|
|
39
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Extract spreadsheet ID from a full Google Sheets URL, or return the input as-is
|
|
4
|
+
* if it's already a plain ID.
|
|
5
|
+
*
|
|
6
|
+
* Google Sheets URLs look like:
|
|
7
|
+
* https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit...
|
|
8
|
+
*/
|
|
9
|
+
export declare function extractSheetId(input: string): string;
|
|
10
|
+
export declare function registerInit(program: Command): void;
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { SheetsAdapter } from "../adapters/sheets/sheets-adapter.js";
|
|
2
|
+
import { defaultSettings } from "../core/defaults.js";
|
|
3
|
+
import { dim, error, success, warn } from "./format.js";
|
|
4
|
+
/**
|
|
5
|
+
* Extract spreadsheet ID from a full Google Sheets URL, or return the input as-is
|
|
6
|
+
* if it's already a plain ID.
|
|
7
|
+
*
|
|
8
|
+
* Google Sheets URLs look like:
|
|
9
|
+
* https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit...
|
|
10
|
+
*/
|
|
11
|
+
export function extractSheetId(input) {
|
|
12
|
+
const match = input.match(/^https?:\/\/docs\.google\.com\/spreadsheets\/d\/([a-zA-Z0-9_-]+)/);
|
|
13
|
+
if (match) {
|
|
14
|
+
return match[1];
|
|
15
|
+
}
|
|
16
|
+
return input;
|
|
17
|
+
}
|
|
18
|
+
export function registerInit(program) {
|
|
19
|
+
program
|
|
20
|
+
.command("init")
|
|
21
|
+
.description("Initialize a sheet with a default Rowbound config")
|
|
22
|
+
.argument("<sheetId>", "Google Sheets spreadsheet ID or URL")
|
|
23
|
+
.option("--tab <name>", "Sheet tab name", "Sheet1")
|
|
24
|
+
.action(async (rawSheetId, opts) => {
|
|
25
|
+
const sheetId = extractSheetId(rawSheetId);
|
|
26
|
+
if (sheetId !== rawSheetId) {
|
|
27
|
+
console.log(warn(`Extracted spreadsheet ID from URL: ${sheetId}`));
|
|
28
|
+
}
|
|
29
|
+
const adapter = new SheetsAdapter();
|
|
30
|
+
const ref = { spreadsheetId: sheetId, sheetName: opts.tab };
|
|
31
|
+
try {
|
|
32
|
+
const existing = await adapter.readConfig(ref);
|
|
33
|
+
if (existing) {
|
|
34
|
+
console.error(error("Config already exists for this sheet.") +
|
|
35
|
+
" Remove it first or use 'rowbound config show' to inspect.");
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Get the tab's GID for v2 config
|
|
40
|
+
const sheets = await adapter.listSheets(sheetId);
|
|
41
|
+
const targetSheet = sheets.find((s) => s.name === opts.tab);
|
|
42
|
+
if (!targetSheet) {
|
|
43
|
+
console.error(error(`Tab "${opts.tab}" not found`) +
|
|
44
|
+
` in spreadsheet ${dim(sheetId)}. Available: ${sheets.map((s) => s.name).join(", ")}`);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const gid = String(targetSheet.gid);
|
|
49
|
+
const defaultConfig = {
|
|
50
|
+
version: "2",
|
|
51
|
+
tabs: {
|
|
52
|
+
[gid]: {
|
|
53
|
+
name: opts.tab,
|
|
54
|
+
columns: {},
|
|
55
|
+
actions: [],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
actions: [],
|
|
59
|
+
settings: defaultSettings,
|
|
60
|
+
};
|
|
61
|
+
await adapter.writeConfig(ref, defaultConfig);
|
|
62
|
+
console.log(success("Initialized Rowbound config for sheet:"), dim(sheetId));
|
|
63
|
+
console.log("Tab:", opts.tab, dim(`(GID: ${gid})`));
|
|
64
|
+
console.log("\nNext steps:\n rowbound config add-action <sheetId> --json '<action>'\n rowbound run <sheetId>");
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
console.error(error("Failed to initialize:"), msg);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|