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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/dist/adapters/adapter.d.ts +1 -0
  4. package/dist/adapters/adapter.js +1 -0
  5. package/dist/adapters/sheets/sheets-adapter.d.ts +66 -0
  6. package/dist/adapters/sheets/sheets-adapter.js +531 -0
  7. package/dist/cli/config.d.ts +2 -0
  8. package/dist/cli/config.js +397 -0
  9. package/dist/cli/env.d.ts +3 -0
  10. package/dist/cli/env.js +103 -0
  11. package/dist/cli/format.d.ts +5 -0
  12. package/dist/cli/format.js +6 -0
  13. package/dist/cli/index.d.ts +2 -0
  14. package/dist/cli/index.js +39 -0
  15. package/dist/cli/init.d.ts +10 -0
  16. package/dist/cli/init.js +72 -0
  17. package/dist/cli/run.d.ts +2 -0
  18. package/dist/cli/run.js +212 -0
  19. package/dist/cli/runs.d.ts +2 -0
  20. package/dist/cli/runs.js +108 -0
  21. package/dist/cli/status.d.ts +2 -0
  22. package/dist/cli/status.js +108 -0
  23. package/dist/cli/sync.d.ts +2 -0
  24. package/dist/cli/sync.js +84 -0
  25. package/dist/cli/watch.d.ts +2 -0
  26. package/dist/cli/watch.js +348 -0
  27. package/dist/core/condition.d.ts +25 -0
  28. package/dist/core/condition.js +66 -0
  29. package/dist/core/defaults.d.ts +3 -0
  30. package/dist/core/defaults.js +7 -0
  31. package/dist/core/engine.d.ts +50 -0
  32. package/dist/core/engine.js +234 -0
  33. package/dist/core/env.d.ts +13 -0
  34. package/dist/core/env.js +72 -0
  35. package/dist/core/exec.d.ts +24 -0
  36. package/dist/core/exec.js +134 -0
  37. package/dist/core/extractor.d.ts +10 -0
  38. package/dist/core/extractor.js +33 -0
  39. package/dist/core/http-client.d.ts +32 -0
  40. package/dist/core/http-client.js +161 -0
  41. package/dist/core/rate-limiter.d.ts +25 -0
  42. package/dist/core/rate-limiter.js +64 -0
  43. package/dist/core/reconcile.d.ts +24 -0
  44. package/dist/core/reconcile.js +192 -0
  45. package/dist/core/run-format.d.ts +39 -0
  46. package/dist/core/run-format.js +201 -0
  47. package/dist/core/run-state.d.ts +64 -0
  48. package/dist/core/run-state.js +141 -0
  49. package/dist/core/run-tracker.d.ts +15 -0
  50. package/dist/core/run-tracker.js +57 -0
  51. package/dist/core/safe-compare.d.ts +8 -0
  52. package/dist/core/safe-compare.js +19 -0
  53. package/dist/core/shell-escape.d.ts +7 -0
  54. package/dist/core/shell-escape.js +9 -0
  55. package/dist/core/tab-resolver.d.ts +17 -0
  56. package/dist/core/tab-resolver.js +44 -0
  57. package/dist/core/template.d.ts +32 -0
  58. package/dist/core/template.js +82 -0
  59. package/dist/core/types.d.ts +105 -0
  60. package/dist/core/types.js +2 -0
  61. package/dist/core/url-guard.d.ts +21 -0
  62. package/dist/core/url-guard.js +184 -0
  63. package/dist/core/validator.d.ts +11 -0
  64. package/dist/core/validator.js +261 -0
  65. package/dist/core/waterfall.d.ts +26 -0
  66. package/dist/core/waterfall.js +55 -0
  67. package/dist/index.d.ts +15 -0
  68. package/dist/index.js +16 -0
  69. package/dist/mcp/server.d.ts +1 -0
  70. package/dist/mcp/server.js +943 -0
  71. 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
+ }
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ export declare function getGlobalEnvPath(): string;
3
+ export declare function registerEnv(program: Command): void;
@@ -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,5 @@
1
+ export declare const success: (s: string) => string;
2
+ export declare const error: (s: string) => string;
3
+ export declare const warn: (s: string) => string;
4
+ export declare const dim: (s: string) => string;
5
+ export declare const bold: (s: string) => string;
@@ -0,0 +1,6 @@
1
+ import pc from "picocolors";
2
+ export const success = (s) => pc.green(s);
3
+ export const error = (s) => pc.red(s);
4
+ export const warn = (s) => pc.yellow(s);
5
+ export const dim = (s) => pc.dim(s);
6
+ export const bold = (s) => pc.bold(s);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerRun(program: Command): void;