@base44-preview/cli 0.0.15-pr.99.a46f627 → 0.0.15-pr.99.a90e72b

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/README.md CHANGED
@@ -72,29 +72,45 @@ base44 deploy
72
72
 
73
73
  ### Connectors
74
74
 
75
- Manage OAuth integrations to connect your app with external services.
75
+ Manage OAuth integrations to connect your app with external services. Connectors are tracked in a local `connectors.jsonc` file and synced with the backend.
76
76
 
77
77
  | Command | Description |
78
78
  |---------|-------------|
79
- | `base44 connectors:add [type]` | Connect an OAuth integration (opens browser for auth) |
80
- | `base44 connectors:list` | List all connected integrations |
81
- | `base44 connectors:remove [type]` | Disconnect an integration |
79
+ | `base44 connectors:add [type]` | Add and connect an OAuth integration |
80
+ | `base44 connectors:list` | List all connectors (local and connected) |
81
+ | `base44 connectors:push` | Connect all pending integrations from local config |
82
+ | `base44 connectors:remove [type]` | Remove an integration |
82
83
  | `base44 connectors:remove [type] --hard` | Permanently remove an integration |
83
84
 
84
85
  **Supported integrations:** Slack, Google Calendar, Google Drive, Gmail, Google Sheets, Google Docs, Google Slides, Notion, Salesforce, HubSpot, LinkedIn, TikTok
85
86
 
86
- **Example:**
87
+ **Example workflow:**
87
88
  ```bash
88
- # Connect Slack (opens browser for OAuth)
89
+ # Add connectors (saves to connectors.jsonc and opens OAuth)
89
90
  base44 connectors:add slack
91
+ base44 connectors:add googlecalendar
90
92
 
91
- # List connected integrations
93
+ # List connectors showing local vs connected status
92
94
  base44 connectors:list
95
+ # Output:
96
+ # ● Slack - user@example.com
97
+ # ○ Google Calendar (not connected)
93
98
 
94
- # Disconnect Slack
99
+ # Connect all pending integrations
100
+ base44 connectors:push
101
+
102
+ # Remove a connector
95
103
  base44 connectors:remove slack
96
104
  ```
97
105
 
106
+ **Local configuration** (`base44/connectors.jsonc`):
107
+ ```jsonc
108
+ {
109
+ "slack": {},
110
+ "googlecalendar": { "scopes": ["calendar.readonly"] }
111
+ }
112
+ ```
113
+
98
114
  Once connected, use the SDK's `connectors.getAccessToken()` to retrieve tokens:
99
115
  ```javascript
100
116
  const token = await base44.connectors.getAccessToken("slack");
package/dist/cli/index.js CHANGED
@@ -5819,6 +5819,97 @@ function handleTupleResult(result, final, index) {
5819
5819
  if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));
5820
5820
  final.value[index] = result.value;
5821
5821
  }
5822
+ const $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
5823
+ $ZodType.init(inst, def);
5824
+ inst._zod.parse = (payload, ctx) => {
5825
+ const input = payload.value;
5826
+ if (!isPlainObject$1(input)) {
5827
+ payload.issues.push({
5828
+ expected: "record",
5829
+ code: "invalid_type",
5830
+ input,
5831
+ inst
5832
+ });
5833
+ return payload;
5834
+ }
5835
+ const proms = [];
5836
+ const values = def.keyType._zod.values;
5837
+ if (values) {
5838
+ payload.value = {};
5839
+ const recordKeys = /* @__PURE__ */ new Set();
5840
+ for (const key of values) if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
5841
+ recordKeys.add(typeof key === "number" ? key.toString() : key);
5842
+ const result = def.valueType._zod.run({
5843
+ value: input[key],
5844
+ issues: []
5845
+ }, ctx);
5846
+ if (result instanceof Promise) proms.push(result.then((result$1) => {
5847
+ if (result$1.issues.length) payload.issues.push(...prefixIssues(key, result$1.issues));
5848
+ payload.value[key] = result$1.value;
5849
+ }));
5850
+ else {
5851
+ if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
5852
+ payload.value[key] = result.value;
5853
+ }
5854
+ }
5855
+ let unrecognized;
5856
+ for (const key in input) if (!recordKeys.has(key)) {
5857
+ unrecognized = unrecognized ?? [];
5858
+ unrecognized.push(key);
5859
+ }
5860
+ if (unrecognized && unrecognized.length > 0) payload.issues.push({
5861
+ code: "unrecognized_keys",
5862
+ input,
5863
+ inst,
5864
+ keys: unrecognized
5865
+ });
5866
+ } else {
5867
+ payload.value = {};
5868
+ for (const key of Reflect.ownKeys(input)) {
5869
+ if (key === "__proto__") continue;
5870
+ let keyResult = def.keyType._zod.run({
5871
+ value: key,
5872
+ issues: []
5873
+ }, ctx);
5874
+ if (keyResult instanceof Promise) throw new Error("Async schemas not supported in object keys currently");
5875
+ if (typeof key === "string" && number$1.test(key) && keyResult.issues.length && keyResult.issues.some((iss) => iss.code === "invalid_type" && iss.expected === "number")) {
5876
+ const retryResult = def.keyType._zod.run({
5877
+ value: Number(key),
5878
+ issues: []
5879
+ }, ctx);
5880
+ if (retryResult instanceof Promise) throw new Error("Async schemas not supported in object keys currently");
5881
+ if (retryResult.issues.length === 0) keyResult = retryResult;
5882
+ }
5883
+ if (keyResult.issues.length) {
5884
+ if (def.mode === "loose") payload.value[key] = input[key];
5885
+ else payload.issues.push({
5886
+ code: "invalid_key",
5887
+ origin: "record",
5888
+ issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
5889
+ input: key,
5890
+ path: [key],
5891
+ inst
5892
+ });
5893
+ continue;
5894
+ }
5895
+ const result = def.valueType._zod.run({
5896
+ value: input[key],
5897
+ issues: []
5898
+ }, ctx);
5899
+ if (result instanceof Promise) proms.push(result.then((result$1) => {
5900
+ if (result$1.issues.length) payload.issues.push(...prefixIssues(key, result$1.issues));
5901
+ payload.value[keyResult.value] = result$1.value;
5902
+ }));
5903
+ else {
5904
+ if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
5905
+ payload.value[keyResult.value] = result.value;
5906
+ }
5907
+ }
5908
+ }
5909
+ if (proms.length) return Promise.all(proms).then(() => payload);
5910
+ return payload;
5911
+ };
5912
+ });
5822
5913
  const $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
5823
5914
  $ZodType.init(inst, def);
5824
5915
  const values = getEnumValues(def.entries);
@@ -7109,6 +7200,39 @@ const tupleProcessor = (schema, ctx, _json, params) => {
7109
7200
  if (typeof minimum === "number") json.minItems = minimum;
7110
7201
  if (typeof maximum === "number") json.maxItems = maximum;
7111
7202
  };
7203
+ const recordProcessor = (schema, ctx, _json, params) => {
7204
+ const json = _json;
7205
+ const def = schema._zod.def;
7206
+ json.type = "object";
7207
+ const keyType = def.keyType;
7208
+ const patterns = keyType._zod.bag?.patterns;
7209
+ if (def.mode === "loose" && patterns && patterns.size > 0) {
7210
+ const valueSchema = process$2(def.valueType, ctx, {
7211
+ ...params,
7212
+ path: [
7213
+ ...params.path,
7214
+ "patternProperties",
7215
+ "*"
7216
+ ]
7217
+ });
7218
+ json.patternProperties = {};
7219
+ for (const pattern of patterns) json.patternProperties[pattern.source] = valueSchema;
7220
+ } else {
7221
+ if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") json.propertyNames = process$2(def.keyType, ctx, {
7222
+ ...params,
7223
+ path: [...params.path, "propertyNames"]
7224
+ });
7225
+ json.additionalProperties = process$2(def.valueType, ctx, {
7226
+ ...params,
7227
+ path: [...params.path, "additionalProperties"]
7228
+ });
7229
+ }
7230
+ const keyValues = keyType._zod.values;
7231
+ if (keyValues) {
7232
+ const validKeyValues = [...keyValues].filter((v$1) => typeof v$1 === "string" || typeof v$1 === "number");
7233
+ if (validKeyValues.length > 0) json.required = validKeyValues;
7234
+ }
7235
+ };
7112
7236
  const nullableProcessor = (schema, ctx, json, params) => {
7113
7237
  const def = schema._zod.def;
7114
7238
  const inner = process$2(def.innerType, ctx, params);
@@ -7638,6 +7762,21 @@ function tuple(items, _paramsOrRest, _params) {
7638
7762
  ...normalizeParams(params)
7639
7763
  });
7640
7764
  }
7765
+ const ZodRecord = /* @__PURE__ */ $constructor("ZodRecord", (inst, def) => {
7766
+ $ZodRecord.init(inst, def);
7767
+ ZodType.init(inst, def);
7768
+ inst._zod.processJSONSchema = (ctx, json, params) => recordProcessor(inst, ctx, json, params);
7769
+ inst.keyType = def.keyType;
7770
+ inst.valueType = def.valueType;
7771
+ });
7772
+ function record(keyType, valueType, params) {
7773
+ return new ZodRecord({
7774
+ type: "record",
7775
+ keyType,
7776
+ valueType,
7777
+ ...normalizeParams(params)
7778
+ });
7779
+ }
7641
7780
  const ZodEnum = /* @__PURE__ */ $constructor("ZodEnum", (inst, def) => {
7642
7781
  $ZodEnum.init(inst, def);
7643
7782
  ZodType.init(inst, def);
@@ -39030,10 +39169,90 @@ function getIntegrationDisplayName(type) {
39030
39169
  return type;
39031
39170
  }
39032
39171
 
39172
+ //#endregion
39173
+ //#region src/core/connectors/config.ts
39174
+ /**
39175
+ * Schema for a single connector configuration
39176
+ */
39177
+ const ConnectorConfigSchema = object({ scopes: array(string()).optional() });
39178
+ /**
39179
+ * Schema for the connectors.jsonc file
39180
+ */
39181
+ const ConnectorsFileSchema = record(string(), ConnectorConfigSchema);
39182
+ const CONNECTORS_FILE_PATTERNS = [`${PROJECT_SUBDIR}/connectors.${CONFIG_FILE_EXTENSION_GLOB}`, `connectors.${CONFIG_FILE_EXTENSION_GLOB}`];
39183
+ /**
39184
+ * Find the connectors config file in the project
39185
+ */
39186
+ async function findConnectorsFile(startPath) {
39187
+ return (await globby(CONNECTORS_FILE_PATTERNS, {
39188
+ cwd: startPath || process.cwd(),
39189
+ absolute: true
39190
+ }))[0] ?? null;
39191
+ }
39192
+ /**
39193
+ * Get the default path for the connectors file
39194
+ */
39195
+ function getDefaultConnectorsPath(projectRoot) {
39196
+ return join(projectRoot || process.cwd(), PROJECT_SUBDIR, "connectors.jsonc");
39197
+ }
39198
+ /**
39199
+ * Read all connectors from the local config file
39200
+ */
39201
+ async function readLocalConnectors(projectRoot) {
39202
+ const filePath = await findConnectorsFile(projectRoot);
39203
+ if (!filePath) return [];
39204
+ const parsed = await readJsonFile(filePath);
39205
+ const result = ConnectorsFileSchema.safeParse(parsed);
39206
+ if (!result.success) throw new Error(`Invalid connectors configuration: ${result.error.message}`);
39207
+ const connectors = [];
39208
+ for (const [type, config$1] of Object.entries(result.data)) {
39209
+ if (!isValidIntegration(type)) throw new Error(`Unknown connector type: ${type}`);
39210
+ connectors.push({
39211
+ type,
39212
+ scopes: config$1.scopes
39213
+ });
39214
+ }
39215
+ return connectors;
39216
+ }
39217
+ /**
39218
+ * Write connectors to the local config file
39219
+ */
39220
+ async function writeLocalConnectors(connectors, projectRoot) {
39221
+ let filePath = await findConnectorsFile(projectRoot);
39222
+ if (!filePath) filePath = getDefaultConnectorsPath(projectRoot);
39223
+ const data = {};
39224
+ for (const connector of connectors) data[connector.type] = { ...connector.scopes && { scopes: connector.scopes } };
39225
+ await writeJsonFile(filePath, data);
39226
+ return filePath;
39227
+ }
39228
+ /**
39229
+ * Add a connector to the local config file
39230
+ */
39231
+ async function addLocalConnector(type, scopes, projectRoot) {
39232
+ const connectors = await readLocalConnectors(projectRoot);
39233
+ const existing = connectors.find((c$1) => c$1.type === type);
39234
+ if (existing) {
39235
+ if (scopes) existing.scopes = scopes;
39236
+ } else connectors.push({
39237
+ type,
39238
+ scopes
39239
+ });
39240
+ return await writeLocalConnectors(connectors, projectRoot);
39241
+ }
39242
+ /**
39243
+ * Remove a connector from the local config file
39244
+ */
39245
+ async function removeLocalConnector(type, projectRoot) {
39246
+ const connectors = await readLocalConnectors(projectRoot);
39247
+ const filtered = connectors.filter((c$1) => c$1.type !== type);
39248
+ if (filtered.length === connectors.length) return null;
39249
+ return await writeLocalConnectors(filtered, projectRoot);
39250
+ }
39251
+
39033
39252
  //#endregion
39034
39253
  //#region src/cli/commands/connectors/add.ts
39035
- const POLL_INTERVAL_MS = 2e3;
39036
- const POLL_TIMEOUT_MS = 300 * 1e3;
39254
+ const POLL_INTERVAL_MS$1 = 2e3;
39255
+ const POLL_TIMEOUT_MS$1 = 300 * 1e3;
39037
39256
  async function promptForIntegrationType() {
39038
39257
  const selected = await ve({
39039
39258
  message: "Select an integration to connect:",
@@ -39063,8 +39282,8 @@ async function waitForOAuthCompletion(integrationType, connectionId) {
39063
39282
  updateMessage("Waiting for authorization in browser...");
39064
39283
  return false;
39065
39284
  }, {
39066
- interval: POLL_INTERVAL_MS,
39067
- timeout: POLL_TIMEOUT_MS
39285
+ interval: POLL_INTERVAL_MS$1,
39286
+ timeout: POLL_TIMEOUT_MS$1
39068
39287
  });
39069
39288
  }, {
39070
39289
  successMessage: "Authorization completed!",
@@ -39105,13 +39324,17 @@ async function addConnector(integrationType) {
39105
39324
  successMessage: `${displayName} OAuth initiated`,
39106
39325
  errorMessage: `Failed to initiate ${displayName} connection`
39107
39326
  });
39108
- if (initiateResponse.already_authorized) return { outroMessage: `Already connected to ${theme.styles.bold(displayName)}` };
39327
+ if (initiateResponse.already_authorized) {
39328
+ await addLocalConnector(selectedType);
39329
+ return { outroMessage: `Already connected to ${theme.styles.bold(displayName)} (added to connectors.jsonc)` };
39330
+ }
39109
39331
  if (initiateResponse.error === "different_user" && initiateResponse.other_user_email) throw new Error(`This app is already connected to ${displayName} by ${initiateResponse.other_user_email}`);
39110
39332
  if (!initiateResponse.redirect_url || !initiateResponse.connection_id) throw new Error("Invalid response from server: missing redirect URL or connection ID");
39111
39333
  M.info(`Opening browser for ${displayName} authorization...`);
39112
39334
  await open_default(initiateResponse.redirect_url);
39113
39335
  const result = await waitForOAuthCompletion(selectedType, initiateResponse.connection_id);
39114
39336
  if (!result.success) throw new Error(result.error || "Authorization failed");
39337
+ await addLocalConnector(selectedType);
39115
39338
  const accountInfo = result.accountEmail ? ` as ${theme.styles.bold(result.accountEmail)}` : "";
39116
39339
  return { outroMessage: `Successfully connected to ${theme.styles.bold(displayName)}${accountInfo}` };
39117
39340
  }
@@ -39124,28 +39347,78 @@ const connectorsAddCommand = new Command("connectors:add").argument("[type]", "I
39124
39347
 
39125
39348
  //#endregion
39126
39349
  //#region src/cli/commands/connectors/list.ts
39350
+ function mergeConnectors(local, backend) {
39351
+ const merged = /* @__PURE__ */ new Map();
39352
+ for (const connector of local) merged.set(connector.type, {
39353
+ type: connector.type,
39354
+ displayName: getIntegrationDisplayName(connector.type),
39355
+ inLocal: true,
39356
+ inBackend: false
39357
+ });
39358
+ for (const connector of backend) {
39359
+ const existing = merged.get(connector.integrationType);
39360
+ if (existing) {
39361
+ existing.inBackend = true;
39362
+ existing.status = connector.status;
39363
+ existing.accountEmail = connector.accountInfo?.email || connector.accountInfo?.name;
39364
+ } else merged.set(connector.integrationType, {
39365
+ type: connector.integrationType,
39366
+ displayName: getIntegrationDisplayName(connector.integrationType),
39367
+ inLocal: false,
39368
+ inBackend: true,
39369
+ status: connector.status,
39370
+ accountEmail: connector.accountInfo?.email || connector.accountInfo?.name
39371
+ });
39372
+ }
39373
+ return Array.from(merged.values());
39374
+ }
39127
39375
  function formatConnectorLine(connector) {
39128
- const name$1 = getIntegrationDisplayName(connector.integrationType);
39129
- const account = connector.accountInfo?.email || connector.accountInfo?.name;
39130
- const status = connector.status.toLowerCase();
39131
- return `${status === "active" ? theme.colors.success("●") : theme.colors.error("○")} ${name$1}${account ? ` - ${account}` : ""}${status !== "active" ? theme.styles.dim(` (${status})`) : ""}`;
39376
+ const { displayName, inLocal, inBackend, status, accountEmail } = connector;
39377
+ const isConnected$1 = inBackend && status?.toLowerCase() === "active";
39378
+ const isPending = inLocal && !inBackend;
39379
+ const isOrphaned = inBackend && !inLocal;
39380
+ let bullet;
39381
+ let statusText = "";
39382
+ if (isConnected$1) {
39383
+ bullet = theme.colors.success("●");
39384
+ if (accountEmail) statusText = ` - ${accountEmail}`;
39385
+ } else if (isPending) {
39386
+ bullet = theme.colors.warning("○");
39387
+ statusText = theme.styles.dim(" (not connected)");
39388
+ } else if (isOrphaned) {
39389
+ bullet = theme.colors.error("○");
39390
+ statusText = theme.styles.dim(" (not in local config)");
39391
+ } else {
39392
+ bullet = theme.colors.error("○");
39393
+ statusText = theme.styles.dim(` (${status || "disconnected"})`);
39394
+ }
39395
+ return `${bullet} ${displayName}${statusText}`;
39132
39396
  }
39133
39397
  async function listConnectorsCommand() {
39134
- const connectors = await runTask("Fetching connectors...", async () => {
39135
- return await listConnectors();
39398
+ const [localConnectors, backendConnectors] = await runTask("Fetching connectors...", async () => {
39399
+ const [local, backend] = await Promise.all([readLocalConnectors().catch(() => []), listConnectors().catch(() => [])]);
39400
+ return [local, backend];
39136
39401
  }, {
39137
39402
  successMessage: "Connectors loaded",
39138
39403
  errorMessage: "Failed to fetch connectors"
39139
39404
  });
39140
- if (connectors.length === 0) {
39405
+ const merged = mergeConnectors(localConnectors, backendConnectors);
39406
+ if (merged.length === 0) {
39141
39407
  M.info("No connectors configured for this app.");
39142
39408
  M.info(`Run ${theme.styles.bold("base44 connectors:add")} to connect an integration.`);
39143
39409
  return { outroMessage: "" };
39144
39410
  }
39145
39411
  console.log();
39146
- for (const connector of connectors) console.log(formatConnectorLine(connector));
39412
+ for (const connector of merged) console.log(formatConnectorLine(connector));
39147
39413
  console.log();
39148
- return { outroMessage: `${connectors.length} connector${connectors.length === 1 ? "" : "s"} configured` };
39414
+ const connected = merged.filter((c$1) => c$1.inBackend && c$1.status?.toLowerCase() === "active").length;
39415
+ const pending = merged.filter((c$1) => c$1.inLocal && !c$1.inBackend).length;
39416
+ let summary = `${connected} connected`;
39417
+ if (pending > 0) {
39418
+ summary += `, ${pending} pending`;
39419
+ M.info(`Run ${theme.styles.bold("base44 connectors:push")} to connect pending integrations.`);
39420
+ }
39421
+ return { outroMessage: summary };
39149
39422
  }
39150
39423
  const connectorsListCommand = new Command("connectors:list").description("List all connected OAuth integrations").action(async () => {
39151
39424
  await runCommand(listConnectorsCommand, {
@@ -39154,56 +39427,195 @@ const connectorsListCommand = new Command("connectors:list").description("List a
39154
39427
  });
39155
39428
  });
39156
39429
 
39430
+ //#endregion
39431
+ //#region src/cli/commands/connectors/push.ts
39432
+ const POLL_INTERVAL_MS = 2e3;
39433
+ const POLL_TIMEOUT_MS = 300 * 1e3;
39434
+ function findPendingConnectors(local, backend) {
39435
+ const connectedTypes = new Set(backend.filter((c$1) => c$1.status.toLowerCase() === "active").map((c$1) => c$1.integrationType));
39436
+ return local.filter((c$1) => !connectedTypes.has(c$1.type)).map((c$1) => ({
39437
+ type: c$1.type,
39438
+ displayName: getIntegrationDisplayName(c$1.type),
39439
+ scopes: c$1.scopes
39440
+ }));
39441
+ }
39442
+ async function connectSingleConnector(connector) {
39443
+ const { type, displayName, scopes } = connector;
39444
+ const initiateResponse = await initiateOAuth(type, scopes || null);
39445
+ if (initiateResponse.already_authorized) return { success: true };
39446
+ if (initiateResponse.error === "different_user") return {
39447
+ success: false,
39448
+ error: `Already connected by ${initiateResponse.other_user_email}`
39449
+ };
39450
+ if (!initiateResponse.redirect_url || !initiateResponse.connection_id) return {
39451
+ success: false,
39452
+ error: "Invalid response from server"
39453
+ };
39454
+ M.info(`Opening browser for ${displayName} authorization...`);
39455
+ await open_default(initiateResponse.redirect_url);
39456
+ let accountEmail;
39457
+ try {
39458
+ await pWaitFor(async () => {
39459
+ const status = await checkOAuthStatus(type, initiateResponse.connection_id);
39460
+ if (status.status === "ACTIVE") {
39461
+ accountEmail = status.accountEmail;
39462
+ return true;
39463
+ }
39464
+ if (status.status === "FAILED") throw new Error(status.error || "Authorization failed");
39465
+ return false;
39466
+ }, {
39467
+ interval: POLL_INTERVAL_MS,
39468
+ timeout: POLL_TIMEOUT_MS
39469
+ });
39470
+ return {
39471
+ success: true,
39472
+ accountEmail
39473
+ };
39474
+ } catch (err) {
39475
+ if (err instanceof Error && err.message.includes("timed out")) return {
39476
+ success: false,
39477
+ error: "Authorization timed out"
39478
+ };
39479
+ return {
39480
+ success: false,
39481
+ error: err instanceof Error ? err.message : "Unknown error"
39482
+ };
39483
+ }
39484
+ }
39485
+ async function pushConnectorsCommand() {
39486
+ const [localConnectors, backendConnectors] = await runTask("Checking connector status...", async () => {
39487
+ const [local, backend] = await Promise.all([readLocalConnectors(), listConnectors().catch(() => [])]);
39488
+ return [local, backend];
39489
+ }, {
39490
+ successMessage: "Status checked",
39491
+ errorMessage: "Failed to check status"
39492
+ });
39493
+ if (localConnectors.length === 0) {
39494
+ M.info("No connectors defined in connectors.jsonc");
39495
+ M.info(`Run ${theme.styles.bold("base44 connectors:add")} to add a connector.`);
39496
+ return { outroMessage: "" };
39497
+ }
39498
+ const pending = findPendingConnectors(localConnectors, backendConnectors);
39499
+ if (pending.length === 0) return { outroMessage: "All connectors are already connected" };
39500
+ console.log();
39501
+ M.info(`${pending.length} connector${pending.length === 1 ? "" : "s"} need${pending.length === 1 ? "s" : ""} to be connected:`);
39502
+ for (const c$1 of pending) console.log(` ${theme.colors.warning("○")} ${c$1.displayName}`);
39503
+ console.log();
39504
+ const shouldProceed = await ye({
39505
+ message: `Connect ${pending.length} integration${pending.length === 1 ? "" : "s"}?`,
39506
+ initialValue: true
39507
+ });
39508
+ if (pD(shouldProceed) || !shouldProceed) return { outroMessage: "Cancelled" };
39509
+ let connected = 0;
39510
+ let failed = 0;
39511
+ for (const connector of pending) {
39512
+ console.log();
39513
+ M.info(`Connecting ${theme.styles.bold(connector.displayName)}...`);
39514
+ const result = await connectSingleConnector(connector);
39515
+ if (result.success) {
39516
+ const accountInfo = result.accountEmail ? ` as ${result.accountEmail}` : "";
39517
+ M.success(`${connector.displayName} connected${accountInfo}`);
39518
+ connected++;
39519
+ } else {
39520
+ M.error(`${connector.displayName} failed: ${result.error}`);
39521
+ failed++;
39522
+ }
39523
+ }
39524
+ console.log();
39525
+ if (failed === 0) return { outroMessage: `Successfully connected ${connected} integration${connected === 1 ? "" : "s"}` };
39526
+ return { outroMessage: `Connected ${connected}, failed ${failed}` };
39527
+ }
39528
+ const connectorsPushCommand = new Command("connectors:push").description("Connect all pending integrations from connectors.jsonc").action(async () => {
39529
+ await runCommand(pushConnectorsCommand, {
39530
+ requireAuth: true,
39531
+ requireAppConfig: true
39532
+ });
39533
+ });
39534
+
39157
39535
  //#endregion
39158
39536
  //#region src/cli/commands/connectors/remove.ts
39537
+ function mergeConnectorsForRemoval(local, backend) {
39538
+ const merged = /* @__PURE__ */ new Map();
39539
+ for (const connector of local) merged.set(connector.type, {
39540
+ type: connector.type,
39541
+ displayName: getIntegrationDisplayName(connector.type),
39542
+ inLocal: true,
39543
+ inBackend: false
39544
+ });
39545
+ for (const connector of backend) {
39546
+ if (!isValidIntegration(connector.integrationType)) continue;
39547
+ const existing = merged.get(connector.integrationType);
39548
+ if (existing) {
39549
+ existing.inBackend = true;
39550
+ existing.accountEmail = connector.accountInfo?.email || connector.accountInfo?.name;
39551
+ } else merged.set(connector.integrationType, {
39552
+ type: connector.integrationType,
39553
+ displayName: getIntegrationDisplayName(connector.integrationType),
39554
+ inLocal: false,
39555
+ inBackend: true,
39556
+ accountEmail: connector.accountInfo?.email || connector.accountInfo?.name
39557
+ });
39558
+ }
39559
+ return Array.from(merged.values());
39560
+ }
39159
39561
  async function promptForConnectorToRemove(connectors) {
39160
39562
  const selected = await ve({
39161
39563
  message: "Select a connector to remove:",
39162
- options: connectors.map((c$1) => ({
39163
- value: c$1.integrationType,
39164
- label: `${getIntegrationDisplayName(c$1.integrationType)}${c$1.accountInfo?.email ? ` (${c$1.accountInfo.email})` : ""}`
39165
- }))
39564
+ options: connectors.map((c$1) => {
39565
+ let label = c$1.displayName;
39566
+ if (c$1.accountEmail) label += ` (${c$1.accountEmail})`;
39567
+ else if (c$1.inLocal && !c$1.inBackend) label += " (not connected)";
39568
+ return {
39569
+ value: c$1.type,
39570
+ label
39571
+ };
39572
+ })
39166
39573
  });
39167
39574
  if (pD(selected)) return null;
39168
39575
  return selected;
39169
39576
  }
39170
39577
  async function removeConnectorCommand(integrationType, options = {}) {
39171
39578
  const isHardDelete = options.hard === true;
39172
- const connectors = await runTask("Fetching connectors...", async () => {
39173
- return await listConnectors();
39579
+ const [localConnectors, backendConnectors] = await runTask("Fetching connectors...", async () => {
39580
+ const [local, backend] = await Promise.all([readLocalConnectors().catch(() => []), listConnectors().catch(() => [])]);
39581
+ return [local, backend];
39174
39582
  }, {
39175
39583
  successMessage: "Connectors loaded",
39176
39584
  errorMessage: "Failed to fetch connectors"
39177
39585
  });
39178
- if (connectors.length === 0) return { outroMessage: "No connectors to remove" };
39586
+ const merged = mergeConnectorsForRemoval(localConnectors, backendConnectors);
39587
+ if (merged.length === 0) return { outroMessage: "No connectors to remove" };
39179
39588
  let selectedType;
39589
+ let selectedConnector;
39180
39590
  if (!integrationType) {
39181
- const prompted = await promptForConnectorToRemove(connectors);
39591
+ const prompted = await promptForConnectorToRemove(merged);
39182
39592
  if (!prompted) return { outroMessage: "Cancelled" };
39183
39593
  selectedType = prompted;
39594
+ selectedConnector = merged.find((c$1) => c$1.type === selectedType);
39184
39595
  } else {
39185
39596
  if (!isValidIntegration(integrationType)) throw new Error(`Invalid connector type: ${integrationType}`);
39186
- if (!connectors.some((c$1) => c$1.integrationType === integrationType)) throw new Error(`No ${getIntegrationDisplayName(integrationType)} connector found for this app`);
39597
+ selectedConnector = merged.find((c$1) => c$1.type === integrationType);
39598
+ if (!selectedConnector) throw new Error(`No ${getIntegrationDisplayName(integrationType)} connector found`);
39187
39599
  selectedType = integrationType;
39188
39600
  }
39189
39601
  const displayName = getIntegrationDisplayName(selectedType);
39190
- const connector = connectors.find((c$1) => c$1.integrationType === selectedType);
39191
- const accountInfo = connector?.accountInfo?.email ? ` (${connector.accountInfo.email})` : "";
39602
+ const accountInfo = selectedConnector?.accountEmail ? ` (${selectedConnector.accountEmail})` : "";
39192
39603
  const shouldRemove = await ye({
39193
- message: `${isHardDelete ? "Permanently remove" : "Disconnect"} ${displayName}${accountInfo}?`,
39604
+ message: `${isHardDelete ? "Permanently remove" : "Remove"} ${displayName}${accountInfo}?`,
39194
39605
  initialValue: false
39195
39606
  });
39196
39607
  if (pD(shouldRemove) || !shouldRemove) return { outroMessage: "Cancelled" };
39197
- await runTask(isHardDelete ? `Removing ${displayName}...` : `Disconnecting ${displayName}...`, async () => {
39198
- if (isHardDelete) await removeConnector(selectedType);
39608
+ await runTask(isHardDelete ? `Removing ${displayName}...` : `Removing ${displayName}...`, async () => {
39609
+ if (selectedConnector?.inBackend) if (isHardDelete) await removeConnector(selectedType);
39199
39610
  else await disconnectConnector(selectedType);
39611
+ await removeLocalConnector(selectedType);
39200
39612
  }, {
39201
- successMessage: isHardDelete ? `${displayName} removed` : `${displayName} disconnected`,
39202
- errorMessage: isHardDelete ? `Failed to remove ${displayName}` : `Failed to disconnect ${displayName}`
39613
+ successMessage: `${displayName} removed`,
39614
+ errorMessage: `Failed to remove ${displayName}`
39203
39615
  });
39204
- return { outroMessage: `Successfully ${isHardDelete ? "removed" : "disconnected"} ${theme.styles.bold(displayName)}` };
39616
+ return { outroMessage: `Successfully removed ${theme.styles.bold(displayName)}` };
39205
39617
  }
39206
- const connectorsRemoveCommand = new Command("connectors:remove").argument("[type]", "Integration type to remove (e.g., slack, notion)").option("--hard", "Permanently remove the connector (cannot be undone)").description("Disconnect an OAuth integration").action(async (type, options) => {
39618
+ const connectorsRemoveCommand = new Command("connectors:remove").argument("[type]", "Integration type to remove (e.g., slack, notion)").option("--hard", "Permanently remove the connector (cannot be undone)").description("Remove an OAuth integration").action(async (type, options) => {
39207
39619
  await runCommand(() => removeConnectorCommand(type, options), {
39208
39620
  requireAuth: true,
39209
39621
  requireAppConfig: true
@@ -39231,6 +39643,7 @@ program.addCommand(functionsDeployCommand);
39231
39643
  program.addCommand(siteDeployCommand);
39232
39644
  program.addCommand(connectorsAddCommand);
39233
39645
  program.addCommand(connectorsListCommand);
39646
+ program.addCommand(connectorsPushCommand);
39234
39647
  program.addCommand(connectorsRemoveCommand);
39235
39648
  program.parse();
39236
39649
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/cli",
3
- "version": "0.0.15-pr.99.a46f627",
3
+ "version": "0.0.15-pr.99.a90e72b",
4
4
  "description": "Base44 CLI - Unified interface for managing Base44 applications",
5
5
  "type": "module",
6
6
  "main": "./dist/cli/index.js",