@base44-preview/cli 0.0.15-pr.99.76064e9 → 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
@@ -70,6 +70,52 @@ base44 deploy
70
70
  |---------|-------------|
71
71
  | `base44 site deploy` | Deploy built site files to Base44 hosting |
72
72
 
73
+ ### Connectors
74
+
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
+
77
+ | Command | Description |
78
+ |---------|-------------|
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 |
83
+ | `base44 connectors:remove [type] --hard` | Permanently remove an integration |
84
+
85
+ **Supported integrations:** Slack, Google Calendar, Google Drive, Gmail, Google Sheets, Google Docs, Google Slides, Notion, Salesforce, HubSpot, LinkedIn, TikTok
86
+
87
+ **Example workflow:**
88
+ ```bash
89
+ # Add connectors (saves to connectors.jsonc and opens OAuth)
90
+ base44 connectors:add slack
91
+ base44 connectors:add googlecalendar
92
+
93
+ # List connectors showing local vs connected status
94
+ base44 connectors:list
95
+ # Output:
96
+ # ● Slack - user@example.com
97
+ # ○ Google Calendar (not connected)
98
+
99
+ # Connect all pending integrations
100
+ base44 connectors:push
101
+
102
+ # Remove a connector
103
+ base44 connectors:remove slack
104
+ ```
105
+
106
+ **Local configuration** (`base44/connectors.jsonc`):
107
+ ```jsonc
108
+ {
109
+ "slack": {},
110
+ "googlecalendar": { "scopes": ["calendar.readonly"] }
111
+ }
112
+ ```
113
+
114
+ Once connected, use the SDK's `connectors.getAccessToken()` to retrieve tokens:
115
+ ```javascript
116
+ const token = await base44.connectors.getAccessToken("slack");
117
+ ```
118
+
73
119
  ## Configuration
74
120
 
75
121
  ### Project Configuration
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);
@@ -38971,6 +39110,19 @@ async function disconnectConnector(integrationType) {
38971
39110
  throw new ConnectorApiError(`Failed to disconnect connector: ${response.status} ${response.statusText}`);
38972
39111
  }
38973
39112
  }
39113
+ /**
39114
+ * Removes (hard delete) a connector integration.
39115
+ * This permanently removes the connector and cannot be undone.
39116
+ */
39117
+ async function removeConnector(integrationType) {
39118
+ const response = await getAppClient().delete(`external-auth/integrations/${integrationType}/remove`, { throwHttpErrors: false });
39119
+ if (!response.ok) {
39120
+ const json = await response.json();
39121
+ const errorResult = ApiErrorSchema.safeParse(json);
39122
+ if (errorResult.success) throw new ConnectorApiError(errorResult.data.error);
39123
+ throw new ConnectorApiError(`Failed to remove connector: ${response.status} ${response.statusText}`);
39124
+ }
39125
+ }
38974
39126
 
38975
39127
  //#endregion
38976
39128
  //#region src/core/connectors/constants.ts
@@ -39017,10 +39169,90 @@ function getIntegrationDisplayName(type) {
39017
39169
  return type;
39018
39170
  }
39019
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
+
39020
39252
  //#endregion
39021
39253
  //#region src/cli/commands/connectors/add.ts
39022
- const POLL_INTERVAL_MS = 2e3;
39023
- const POLL_TIMEOUT_MS = 300 * 1e3;
39254
+ const POLL_INTERVAL_MS$1 = 2e3;
39255
+ const POLL_TIMEOUT_MS$1 = 300 * 1e3;
39024
39256
  async function promptForIntegrationType() {
39025
39257
  const selected = await ve({
39026
39258
  message: "Select an integration to connect:",
@@ -39050,8 +39282,8 @@ async function waitForOAuthCompletion(integrationType, connectionId) {
39050
39282
  updateMessage("Waiting for authorization in browser...");
39051
39283
  return false;
39052
39284
  }, {
39053
- interval: POLL_INTERVAL_MS,
39054
- timeout: POLL_TIMEOUT_MS
39285
+ interval: POLL_INTERVAL_MS$1,
39286
+ timeout: POLL_TIMEOUT_MS$1
39055
39287
  });
39056
39288
  }, {
39057
39289
  successMessage: "Authorization completed!",
@@ -39092,13 +39324,17 @@ async function addConnector(integrationType) {
39092
39324
  successMessage: `${displayName} OAuth initiated`,
39093
39325
  errorMessage: `Failed to initiate ${displayName} connection`
39094
39326
  });
39095
- 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
+ }
39096
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}`);
39097
39332
  if (!initiateResponse.redirect_url || !initiateResponse.connection_id) throw new Error("Invalid response from server: missing redirect URL or connection ID");
39098
39333
  M.info(`Opening browser for ${displayName} authorization...`);
39099
39334
  await open_default(initiateResponse.redirect_url);
39100
39335
  const result = await waitForOAuthCompletion(selectedType, initiateResponse.connection_id);
39101
39336
  if (!result.success) throw new Error(result.error || "Authorization failed");
39337
+ await addLocalConnector(selectedType);
39102
39338
  const accountInfo = result.accountEmail ? ` as ${theme.styles.bold(result.accountEmail)}` : "";
39103
39339
  return { outroMessage: `Successfully connected to ${theme.styles.bold(displayName)}${accountInfo}` };
39104
39340
  }
@@ -39111,64 +39347,78 @@ const connectorsAddCommand = new Command("connectors:add").argument("[type]", "I
39111
39347
 
39112
39348
  //#endregion
39113
39349
  //#region src/cli/commands/connectors/list.ts
39114
- function formatDate(dateString) {
39115
- if (!dateString) return "-";
39116
- try {
39117
- return new Date(dateString).toLocaleDateString("en-US", {
39118
- year: "numeric",
39119
- month: "short",
39120
- day: "numeric"
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
39121
39371
  });
39122
- } catch {
39123
- return dateString;
39124
39372
  }
39125
- }
39126
- function formatStatus(status) {
39127
- const normalized = status.toLowerCase();
39128
- if (normalized === "active" || normalized === "connected") return theme.colors.success("● active");
39129
- if (normalized === "expired") return theme.colors.warning("● expired");
39130
- if (normalized === "failed" || normalized === "disconnected") return theme.colors.error("● disconnected");
39131
- return status;
39373
+ return Array.from(merged.values());
39374
+ }
39375
+ function formatConnectorLine(connector) {
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
- console.log(theme.styles.bold("Connected Integrations:"));
39412
+ for (const connector of merged) console.log(formatConnectorLine(connector));
39147
39413
  console.log();
39148
- const headers = [
39149
- "Type",
39150
- "Account",
39151
- "Status",
39152
- "Connected"
39153
- ];
39154
- const colWidths = [
39155
- 20,
39156
- 30,
39157
- 15,
39158
- 15
39159
- ];
39160
- const headerRow = headers.map((h$2, i$1) => h$2.padEnd(colWidths[i$1])).join(" ");
39161
- console.log(theme.styles.dim(headerRow));
39162
- console.log(theme.styles.dim("─".repeat(headerRow.length)));
39163
- for (const connector of connectors) {
39164
- const type = getIntegrationDisplayName(connector.integrationType).padEnd(colWidths[0]);
39165
- const account = (connector.accountInfo?.email || connector.accountInfo?.name || "-").padEnd(colWidths[1]);
39166
- const status = formatStatus(connector.status);
39167
- const connected = formatDate(connector.connectedAt).padEnd(colWidths[3]);
39168
- console.log(`${type} ${account} ${status.padEnd(colWidths[2] + 10)} ${connected}`);
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.`);
39169
39420
  }
39170
- console.log();
39171
- return { outroMessage: `${connectors.length} connector${connectors.length === 1 ? "" : "s"} configured` };
39421
+ return { outroMessage: summary };
39172
39422
  }
39173
39423
  const connectorsListCommand = new Command("connectors:list").description("List all connected OAuth integrations").action(async () => {
39174
39424
  await runCommand(listConnectorsCommand, {
@@ -39177,54 +39427,196 @@ const connectorsListCommand = new Command("connectors:list").description("List a
39177
39427
  });
39178
39428
  });
39179
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
+
39180
39535
  //#endregion
39181
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
+ }
39182
39561
  async function promptForConnectorToRemove(connectors) {
39183
39562
  const selected = await ve({
39184
39563
  message: "Select a connector to remove:",
39185
- options: connectors.map((c$1) => ({
39186
- value: c$1.integrationType,
39187
- label: `${getIntegrationDisplayName(c$1.integrationType)}${c$1.accountInfo?.email ? ` (${c$1.accountInfo.email})` : ""}`
39188
- }))
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
+ })
39189
39573
  });
39190
39574
  if (pD(selected)) return null;
39191
39575
  return selected;
39192
39576
  }
39193
- async function removeConnectorCommand(integrationType) {
39194
- const connectors = await runTask("Fetching connectors...", async () => {
39195
- return await listConnectors();
39577
+ async function removeConnectorCommand(integrationType, options = {}) {
39578
+ const isHardDelete = options.hard === true;
39579
+ const [localConnectors, backendConnectors] = await runTask("Fetching connectors...", async () => {
39580
+ const [local, backend] = await Promise.all([readLocalConnectors().catch(() => []), listConnectors().catch(() => [])]);
39581
+ return [local, backend];
39196
39582
  }, {
39197
39583
  successMessage: "Connectors loaded",
39198
39584
  errorMessage: "Failed to fetch connectors"
39199
39585
  });
39200
- 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" };
39201
39588
  let selectedType;
39589
+ let selectedConnector;
39202
39590
  if (!integrationType) {
39203
- const prompted = await promptForConnectorToRemove(connectors);
39591
+ const prompted = await promptForConnectorToRemove(merged);
39204
39592
  if (!prompted) return { outroMessage: "Cancelled" };
39205
39593
  selectedType = prompted;
39594
+ selectedConnector = merged.find((c$1) => c$1.type === selectedType);
39206
39595
  } else {
39207
39596
  if (!isValidIntegration(integrationType)) throw new Error(`Invalid connector type: ${integrationType}`);
39208
- 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`);
39209
39599
  selectedType = integrationType;
39210
39600
  }
39211
39601
  const displayName = getIntegrationDisplayName(selectedType);
39212
- const connector = connectors.find((c$1) => c$1.integrationType === selectedType);
39602
+ const accountInfo = selectedConnector?.accountEmail ? ` (${selectedConnector.accountEmail})` : "";
39213
39603
  const shouldRemove = await ye({
39214
- message: `Disconnect ${displayName}${connector?.accountInfo?.email ? ` (${connector.accountInfo.email})` : ""}?`,
39604
+ message: `${isHardDelete ? "Permanently remove" : "Remove"} ${displayName}${accountInfo}?`,
39215
39605
  initialValue: false
39216
39606
  });
39217
39607
  if (pD(shouldRemove) || !shouldRemove) return { outroMessage: "Cancelled" };
39218
- await runTask(`Disconnecting ${displayName}...`, async () => {
39219
- await disconnectConnector(selectedType);
39608
+ await runTask(isHardDelete ? `Removing ${displayName}...` : `Removing ${displayName}...`, async () => {
39609
+ if (selectedConnector?.inBackend) if (isHardDelete) await removeConnector(selectedType);
39610
+ else await disconnectConnector(selectedType);
39611
+ await removeLocalConnector(selectedType);
39220
39612
  }, {
39221
- successMessage: `${displayName} disconnected`,
39222
- errorMessage: `Failed to disconnect ${displayName}`
39613
+ successMessage: `${displayName} removed`,
39614
+ errorMessage: `Failed to remove ${displayName}`
39223
39615
  });
39224
- return { outroMessage: `Successfully disconnected ${theme.styles.bold(displayName)}` };
39616
+ return { outroMessage: `Successfully removed ${theme.styles.bold(displayName)}` };
39225
39617
  }
39226
- const connectorsRemoveCommand = new Command("connectors:remove").argument("[type]", "Integration type to remove (e.g., slack, notion)").description("Disconnect an OAuth integration").action(async (type) => {
39227
- await runCommand(() => removeConnectorCommand(type), {
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) => {
39619
+ await runCommand(() => removeConnectorCommand(type, options), {
39228
39620
  requireAuth: true,
39229
39621
  requireAppConfig: true
39230
39622
  });
@@ -39251,6 +39643,7 @@ program.addCommand(functionsDeployCommand);
39251
39643
  program.addCommand(siteDeployCommand);
39252
39644
  program.addCommand(connectorsAddCommand);
39253
39645
  program.addCommand(connectorsListCommand);
39646
+ program.addCommand(connectorsPushCommand);
39254
39647
  program.addCommand(connectorsRemoveCommand);
39255
39648
  program.parse();
39256
39649
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/cli",
3
- "version": "0.0.15-pr.99.76064e9",
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",