@lobu/cli 6.0.1 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -27
- package/dist/bundled-skills/lobu/SKILL.md +11 -11
- package/dist/commands/_lib/apply/apply-cmd.d.ts +38 -0
- package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
- package/dist/commands/_lib/apply/apply-cmd.js +574 -40
- package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
- package/dist/commands/_lib/apply/client.d.ts +180 -1
- package/dist/commands/_lib/apply/client.d.ts.map +1 -1
- package/dist/commands/_lib/apply/client.js +308 -28
- package/dist/commands/_lib/apply/client.js.map +1 -1
- package/dist/commands/_lib/apply/desired-state.d.ts +134 -3
- package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
- package/dist/commands/_lib/apply/desired-state.js +703 -89
- package/dist/commands/_lib/apply/desired-state.js.map +1 -1
- package/dist/commands/_lib/apply/diff.d.ts +61 -3
- package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
- package/dist/commands/_lib/apply/diff.js +382 -92
- package/dist/commands/_lib/apply/diff.js.map +1 -1
- package/dist/commands/_lib/apply/prompt.d.ts +6 -0
- package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
- package/dist/commands/_lib/apply/prompt.js +16 -0
- package/dist/commands/_lib/apply/prompt.js.map +1 -1
- package/dist/commands/_lib/apply/render.d.ts +9 -0
- package/dist/commands/_lib/apply/render.d.ts.map +1 -1
- package/dist/commands/_lib/apply/render.js +80 -3
- package/dist/commands/_lib/apply/render.js.map +1 -1
- package/dist/commands/agent.d.ts +7 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +65 -1
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/chat.d.ts +12 -9
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +125 -57
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/dev.d.ts +23 -7
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +197 -49
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +136 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eval.d.ts +8 -0
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +72 -6
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init.d.ts +22 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +355 -182
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +11 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +28 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +14 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.js +3 -3
- package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
- package/dist/commands/memory/_lib/mcp.d.ts +2 -2
- package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
- package/dist/commands/memory/_lib/mcp.js +24 -12
- package/dist/commands/memory/_lib/mcp.js.map +1 -1
- package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
- package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
- package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
- package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
- package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
- package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
- package/dist/commands/memory/_lib/schema.d.ts +29 -2
- package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
- package/dist/commands/memory/_lib/schema.js +121 -5
- package/dist/commands/memory/_lib/schema.js.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.js +46 -24
- package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
- package/dist/commands/memory/run.d.ts.map +1 -1
- package/dist/commands/memory/run.js +2 -2
- package/dist/commands/memory/run.js.map +1 -1
- package/dist/commands/org.d.ts +4 -0
- package/dist/commands/org.d.ts.map +1 -1
- package/dist/commands/org.js +10 -0
- package/dist/commands/org.js.map +1 -1
- package/dist/commands/platforms/platform-prompts.d.ts +0 -1
- package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
- package/dist/commands/platforms/platform-prompts.js +54 -8
- package/dist/commands/platforms/platform-prompts.js.map +1 -1
- package/dist/commands/telemetry.d.ts +10 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/telemetry.js +68 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/commands/token.d.ts +9 -0
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +54 -0
- package/dist/commands/token.js.map +1 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/connectors/README.md +534 -0
- package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
- package/dist/connectors/apple_health.ts +138 -0
- package/dist/connectors/apple_screen_time.ts +82 -0
- package/dist/connectors/browser-scraper-utils.ts +246 -0
- package/dist/connectors/capterra.ts +277 -0
- package/dist/connectors/g2.ts +290 -0
- package/dist/connectors/github.ts +1530 -0
- package/dist/connectors/glassdoor.ts +295 -0
- package/dist/connectors/gmaps.ts +197 -0
- package/dist/connectors/google_calendar.ts +641 -0
- package/dist/connectors/google_gmail.ts +754 -0
- package/dist/connectors/google_photos.ts +776 -0
- package/dist/connectors/google_play.ts +349 -0
- package/dist/connectors/hackernews.ts +471 -0
- package/dist/connectors/index.ts +28 -0
- package/dist/connectors/ios_appstore.ts +226 -0
- package/dist/connectors/linkedin.ts +494 -0
- package/dist/connectors/local_directory.ts +91 -0
- package/dist/connectors/microsoft_outlook.ts +410 -0
- package/dist/connectors/producthunt.ts +471 -0
- package/dist/connectors/reddit.ts +600 -0
- package/dist/connectors/revolut.ts +572 -0
- package/dist/connectors/rss.ts +448 -0
- package/dist/connectors/spotify.ts +590 -0
- package/dist/connectors/trustpilot.ts +203 -0
- package/dist/connectors/website.ts +629 -0
- package/dist/connectors/whatsapp.ts +1081 -0
- package/dist/connectors/whatsapp_local.ts +125 -0
- package/dist/connectors/x.ts +536 -0
- package/dist/connectors/youtube.ts +666 -0
- package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
- package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
- package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
- package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
- package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
- package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
- package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
- package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
- package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
- package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
- package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
- package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
- package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
- package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
- package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
- package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
- package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
- package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
- package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
- package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
- package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
- package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
- package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
- package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
- package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
- package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
- package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
- package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
- package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
- package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
- package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
- package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
- package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
- package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
- package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
- package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
- package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
- package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
- package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
- package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
- package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
- package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
- package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
- package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
- package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
- package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
- package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
- package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
- package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
- package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
- package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
- package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
- package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
- package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
- package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
- package/dist/eval/types.d.ts +2 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +210 -132
- package/dist/index.js.map +1 -1
- package/dist/internal/api-client.d.ts +4 -8
- package/dist/internal/api-client.d.ts.map +1 -1
- package/dist/internal/api-client.js +1 -1
- package/dist/internal/api-client.js.map +1 -1
- package/dist/internal/context.js +2 -2
- package/dist/internal/context.js.map +1 -1
- package/dist/internal/credentials.d.ts.map +1 -1
- package/dist/internal/credentials.js +6 -1
- package/dist/internal/credentials.js.map +1 -1
- package/dist/internal/gateway-url.d.ts +14 -0
- package/dist/internal/gateway-url.d.ts.map +1 -1
- package/dist/internal/gateway-url.js +19 -0
- package/dist/internal/gateway-url.js.map +1 -1
- package/dist/internal/index.d.ts +3 -4
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +3 -3
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/oauth.d.ts +6 -5
- package/dist/internal/oauth.d.ts.map +1 -1
- package/dist/internal/oauth.js +2 -2
- package/dist/internal/project-link.d.ts +10 -0
- package/dist/internal/project-link.d.ts.map +1 -0
- package/dist/internal/project-link.js +48 -0
- package/dist/internal/project-link.js.map +1 -0
- package/dist/providers.json +2 -2
- package/dist/server.bundle.mjs +31654 -30866
- package/dist/start-local.bundle.mjs +74409 -0
- package/dist/templates/README.md.tmpl +10 -11
- package/dist/templates/TESTING.md.tmpl +9 -9
- package/package.json +15 -13
- package/dist/__tests__/chat.integration.test.d.ts +0 -2
- package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
- package/dist/__tests__/chat.integration.test.js +0 -337
- package/dist/__tests__/chat.integration.test.js.map +0 -1
- package/dist/__tests__/dev.test.d.ts +0 -2
- package/dist/__tests__/dev.test.d.ts.map +0 -1
- package/dist/__tests__/dev.test.js +0 -25
- package/dist/__tests__/dev.test.js.map +0 -1
- package/dist/__tests__/init-memory.test.d.ts +0 -2
- package/dist/__tests__/init-memory.test.d.ts.map +0 -1
- package/dist/__tests__/init-memory.test.js +0 -45
- package/dist/__tests__/init-memory.test.js.map +0 -1
- package/dist/__tests__/token.test.d.ts +0 -2
- package/dist/__tests__/token.test.d.ts.map +0 -1
- package/dist/__tests__/token.test.js +0 -52
- package/dist/__tests__/token.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
- package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
- package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
- package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
- package/dist/commands/apply.d.ts +0 -3
- package/dist/commands/apply.d.ts.map +0 -1
- package/dist/commands/apply.js +0 -5
- package/dist/commands/apply.js.map +0 -1
- package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
- package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
- package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
- package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
- package/dist/internal/__tests__/api-client.test.d.ts +0 -2
- package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
- package/dist/internal/__tests__/api-client.test.js +0 -95
- package/dist/internal/__tests__/api-client.test.js.map +0 -1
- package/dist/internal/__tests__/context.test.d.ts +0 -2
- package/dist/internal/__tests__/context.test.d.ts.map +0 -1
- package/dist/internal/__tests__/context.test.js +0 -77
- package/dist/internal/__tests__/context.test.js.map +0 -1
|
@@ -1,25 +1,165 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import chalk from "chalk";
|
|
4
|
+
import { resolveContext } from "../../../internal/context.js";
|
|
5
|
+
import { loadProjectLink } from "../../../internal/project-link.js";
|
|
6
|
+
import { CONFIG_FILENAME } from "../../../config/loader.js";
|
|
2
7
|
import { ApiError, ValidationError } from "../../memory/_lib/errors.js";
|
|
3
8
|
import { printError, printText } from "../../memory/_lib/output.js";
|
|
4
9
|
import { resolveApplyClient, } from "./client.js";
|
|
5
10
|
import { computeDiff, } from "./diff.js";
|
|
6
|
-
import { loadDesiredState } from "./desired-state.js";
|
|
7
|
-
import { confirmPlan } from "./prompt.js";
|
|
8
|
-
import { renderMissingSecrets, renderPlan, renderProgress } from "./render.js";
|
|
9
|
-
// ── Required-secrets check ─────────────────────────────────────────────────
|
|
11
|
+
import { loadDesiredState, resolveConnectorSchemas, validateAuthProfileAgainstConnector, validateConnectionAgainstConnector, } from "./desired-state.js";
|
|
12
|
+
import { confirmCustomConnectors, confirmPlan } from "./prompt.js";
|
|
13
|
+
import { renderMissingSecrets, renderPlan, renderPostApplyPunchList, renderProgress, } from "./render.js";
|
|
10
14
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* must satisfy at runtime — surfacing it pre-mutation gives the operator
|
|
15
|
-
* a cleaner failure than a silent empty-string config push.
|
|
16
|
-
*
|
|
17
|
-
* Plan §7 reserves cloud-side secret-list cross-checks for v3.
|
|
15
|
+
* Write `organization_id = "<id>"` into the `[memory]` section of lobu.toml —
|
|
16
|
+
* replacing an existing value or inserting it just under the `[memory]` header.
|
|
17
|
+
* Surgical text edit; preserves comments and the rest of the file.
|
|
18
18
|
*/
|
|
19
|
+
async function writeMemoryOrganizationId(cwd, organizationId) {
|
|
20
|
+
const path = join(cwd, CONFIG_FILENAME);
|
|
21
|
+
const raw = await readFile(path, "utf-8");
|
|
22
|
+
const line = `organization_id = "${organizationId}"`;
|
|
23
|
+
if (/^\s*organization_id\s*=.*$/m.test(raw)) {
|
|
24
|
+
const next = raw.replace(/^\s*organization_id\s*=.*$/m, line);
|
|
25
|
+
if (next !== raw)
|
|
26
|
+
await writeFile(path, next);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const header = raw.match(/^\[memory\][^\n]*$/m);
|
|
30
|
+
if (!header || header.index === undefined)
|
|
31
|
+
return;
|
|
32
|
+
const at = header.index + header[0].length;
|
|
33
|
+
await writeFile(path, `${raw.slice(0, at)}\n${line}${raw.slice(at)}`);
|
|
34
|
+
}
|
|
35
|
+
// ── Required-secrets check ─────────────────────────────────────────────────
|
|
19
36
|
function checkRequiredSecrets(state) {
|
|
20
37
|
const missing = state.requiredSecrets.filter((name) => process.env[name] === undefined || process.env[name] === "");
|
|
21
38
|
return { missing };
|
|
22
39
|
}
|
|
40
|
+
// ── source_url: confirmed-before-fetch, https-only, bounded fetch ──────────
|
|
41
|
+
const CONNECTOR_SOURCE_MAX_BYTES = 2 * 1024 * 1024; // 2 MiB
|
|
42
|
+
const CONNECTOR_SOURCE_FETCH_TIMEOUT_MS = 15_000;
|
|
43
|
+
/**
|
|
44
|
+
* Read a response body as a stream, counting *bytes* and aborting as soon as
|
|
45
|
+
* the running total exceeds `maxBytes` — before buffering the rest. Decodes to
|
|
46
|
+
* UTF-8 text only after the (bounded) body is in hand. Exported for testing.
|
|
47
|
+
*/
|
|
48
|
+
export async function readBoundedBody(res, maxBytes, onOverflow) {
|
|
49
|
+
const reader = res.body?.getReader();
|
|
50
|
+
if (!reader) {
|
|
51
|
+
// No streaming body (rare; e.g. a mock). Fall back to text() + a byte check.
|
|
52
|
+
const text = await res.text();
|
|
53
|
+
if (Buffer.byteLength(text, "utf8") > maxBytes)
|
|
54
|
+
onOverflow();
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
const chunks = [];
|
|
58
|
+
let total = 0;
|
|
59
|
+
try {
|
|
60
|
+
for (;;) {
|
|
61
|
+
const { done, value } = await reader.read();
|
|
62
|
+
if (done)
|
|
63
|
+
break;
|
|
64
|
+
if (value) {
|
|
65
|
+
total += value.byteLength;
|
|
66
|
+
if (total > maxBytes) {
|
|
67
|
+
await reader.cancel().catch(() => undefined);
|
|
68
|
+
onOverflow();
|
|
69
|
+
}
|
|
70
|
+
chunks.push(value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
try {
|
|
76
|
+
reader.releaseLock?.();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// already released by cancel() — ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
|
|
83
|
+
}
|
|
84
|
+
async function materializeConnectorSource(defs, fetchImpl) {
|
|
85
|
+
for (const def of defs) {
|
|
86
|
+
if (def.sourceCode !== undefined || !def.sourceUrl)
|
|
87
|
+
continue;
|
|
88
|
+
let url;
|
|
89
|
+
try {
|
|
90
|
+
url = new URL(def.sourceUrl);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new ValidationError(`${def.sourceFile}: connector source_url is not a valid URL: ${def.sourceUrl}`);
|
|
94
|
+
}
|
|
95
|
+
if (url.protocol !== "https:") {
|
|
96
|
+
throw new ValidationError(`${def.sourceFile}: connector source_url must use https (got ${url.protocol}//): ${def.sourceUrl}`);
|
|
97
|
+
}
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
// Single timer covering the whole exchange — connect AND body consumption.
|
|
100
|
+
const timer = setTimeout(() => controller.abort(), CONNECTOR_SOURCE_FETCH_TIMEOUT_MS);
|
|
101
|
+
let body;
|
|
102
|
+
try {
|
|
103
|
+
let res;
|
|
104
|
+
try {
|
|
105
|
+
res = await fetchImpl(def.sourceUrl, { signal: controller.signal });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
throw new ValidationError(`${def.sourceFile}: failed to fetch connector source_url ${def.sourceUrl} — ${err instanceof Error ? err.message : String(err)}`);
|
|
109
|
+
}
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
throw new ValidationError(`${def.sourceFile}: connector source_url ${def.sourceUrl} returned HTTP ${res.status} ${res.statusText}`);
|
|
112
|
+
}
|
|
113
|
+
const contentType = (res.headers.get("content-type") ?? "").toLowerCase();
|
|
114
|
+
if (contentType &&
|
|
115
|
+
!/(text\/|application\/(typescript|javascript|x-typescript|octet-stream))/.test(contentType)) {
|
|
116
|
+
throw new ValidationError(`${def.sourceFile}: connector source_url ${def.sourceUrl} returned unexpected content-type "${contentType}" — expected text/*, application/typescript, or application/javascript`);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
body = await readBoundedBody(res, CONNECTOR_SOURCE_MAX_BYTES, () => {
|
|
120
|
+
throw new ValidationError(`${def.sourceFile}: connector source_url ${def.sourceUrl} body exceeds the ${CONNECTOR_SOURCE_MAX_BYTES}-byte cap`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (err instanceof ValidationError)
|
|
125
|
+
throw err;
|
|
126
|
+
throw new ValidationError(`${def.sourceFile}: failed to read connector source_url ${def.sourceUrl} — ${err instanceof Error ? err.message : String(err)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
}
|
|
132
|
+
if (!body.trim()) {
|
|
133
|
+
throw new ValidationError(`${def.sourceFile}: connector source_url ${def.sourceUrl} returned an empty body`);
|
|
134
|
+
}
|
|
135
|
+
def.sourceCode = body;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Warn + require confirmation BEFORE the CLI fetches any `source_url` or
|
|
140
|
+
* uploads any custom connector source for compilation on the gateway.
|
|
141
|
+
*
|
|
142
|
+
* SECURITY: `install_connector` compiles + imports + instantiates the connector
|
|
143
|
+
* runtime class on the gateway. The server-side compiler currently runs with
|
|
144
|
+
* full gateway env/fs/network and only blocks relative imports — this consent
|
|
145
|
+
* gate is the operator's last line of defence. (TODO(security): sandbox the
|
|
146
|
+
* server-side connector compiler — tracked separately, out of scope here.)
|
|
147
|
+
*/
|
|
148
|
+
async function confirmCustomConnectorSource(defs, yes) {
|
|
149
|
+
if (defs.length === 0)
|
|
150
|
+
return;
|
|
151
|
+
printText(chalk.yellow(`\n ⚠ This project ships ${defs.length} custom connector source ${defs.length === 1 ? "definition" : "definitions"}:`));
|
|
152
|
+
for (const def of defs) {
|
|
153
|
+
printText(chalk.yellow(def.sourceUrl
|
|
154
|
+
? ` - ${def.sourceFile} → fetches ${def.sourceUrl}`
|
|
155
|
+
: ` - ${def.sourceFile}`));
|
|
156
|
+
}
|
|
157
|
+
printText(chalk.yellow(" `lobu apply` will fetch (https) and UPLOAD this source; the gateway will COMPILE and EXECUTE it.\n Only proceed if you trust this code."));
|
|
158
|
+
const ok = await confirmCustomConnectors(yes);
|
|
159
|
+
if (!ok) {
|
|
160
|
+
throw new ValidationError("Cancelled — custom connectors not confirmed.");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
23
163
|
// ── Snapshot ───────────────────────────────────────────────────────────────
|
|
24
164
|
async function fetchRemoteSnapshot(client, state, only) {
|
|
25
165
|
const agents = only === "memory" ? [] : await client.listAgents();
|
|
@@ -28,8 +168,6 @@ async function fetchRemoteSnapshot(client, state, only) {
|
|
|
28
168
|
if (only !== "memory") {
|
|
29
169
|
const desiredAgentIds = state.agents.map((a) => a.metadata.agentId);
|
|
30
170
|
const remoteAgentIds = new Set(agents.map((a) => a.agentId));
|
|
31
|
-
// Only GET settings for agents that exist; new agents have no remote
|
|
32
|
-
// settings to compare against.
|
|
33
171
|
const targetAgentIds = desiredAgentIds.filter((id) => remoteAgentIds.has(id));
|
|
34
172
|
for (const agentId of targetAgentIds) {
|
|
35
173
|
agentSettings.set(agentId, await client.getAgentSettings(agentId));
|
|
@@ -38,21 +176,155 @@ async function fetchRemoteSnapshot(client, state, only) {
|
|
|
38
176
|
}
|
|
39
177
|
const entityTypes = only === "agents" ? [] : await client.listEntityTypes();
|
|
40
178
|
const relationshipTypes = only === "agents" ? [] : await client.listRelationshipTypes();
|
|
179
|
+
const watchers = only === "agents" ? [] : await client.listWatchers();
|
|
180
|
+
// Connectors run only on a full apply (`--only` skips them).
|
|
181
|
+
const hasConnectors = state.connectors.definitions.length > 0 ||
|
|
182
|
+
state.connectors.authProfiles.length > 0 ||
|
|
183
|
+
state.connectors.connections.length > 0;
|
|
184
|
+
const connectorDefinitions = only || !hasConnectors ? [] : await client.listConnectorDefinitions(true);
|
|
185
|
+
const authProfiles = only || !hasConnectors ? [] : await client.listAuthProfiles();
|
|
186
|
+
const connections = only || !hasConnectors ? [] : await client.listConnections();
|
|
187
|
+
const feedsByConnectionId = new Map();
|
|
188
|
+
if (!only && hasConnectors) {
|
|
189
|
+
const desiredConnSlugs = new Set(state.connectors.connections.map((c) => c.slug));
|
|
190
|
+
for (const conn of connections) {
|
|
191
|
+
if (!desiredConnSlugs.has(conn.slug))
|
|
192
|
+
continue;
|
|
193
|
+
feedsByConnectionId.set(conn.id, await client.listFeeds(conn.id));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
41
196
|
return {
|
|
42
197
|
agents,
|
|
43
198
|
agentSettings,
|
|
44
199
|
platformsByAgent,
|
|
45
200
|
entityTypes,
|
|
46
201
|
relationshipTypes,
|
|
202
|
+
watchers,
|
|
203
|
+
connectorDefinitions,
|
|
204
|
+
authProfiles,
|
|
205
|
+
connections,
|
|
206
|
+
feedsByConnectionId,
|
|
47
207
|
};
|
|
48
208
|
}
|
|
209
|
+
// ── Connector definition install (runs INSIDE executePlan, after confirm) ──
|
|
49
210
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
211
|
+
* Install/update the project's custom connector definitions, then any *bundled*
|
|
212
|
+
* connectors referenced by an auth-profile / connection (the server only
|
|
213
|
+
* resolves *installed* defs in `create_auth_profile` / `create_feed`, not the
|
|
214
|
+
* catalog). Returns the fresh connector-definition catalog.
|
|
53
215
|
*/
|
|
54
|
-
async function
|
|
216
|
+
async function installConnectorDefinitions(client, state, catalog, plan) {
|
|
217
|
+
const installedKeys = new Set(catalog.filter((d) => d.installed).map((d) => d.key));
|
|
218
|
+
// Connector keys this project supplies its own source for — these must NEVER
|
|
219
|
+
// be replaced by a bundled `source_uri` install, even if a bundled connector
|
|
220
|
+
// shares the key. (`null` keys — auto-discovered `*.connector.ts` whose key
|
|
221
|
+
// the server resolves at compile time — are added to this set below as soon
|
|
222
|
+
// as `install_connector` returns the resolved key, so the bundled loop can't
|
|
223
|
+
// race them either.)
|
|
224
|
+
const locallySuppliedKeys = new Set(state.connectors.definitions
|
|
225
|
+
.map((d) => d.key)
|
|
226
|
+
.filter((k) => !!k));
|
|
227
|
+
let mutated = false;
|
|
228
|
+
// Iterate the plan's connector-definition rows so progress mirrors the plan.
|
|
229
|
+
for (const row of plan.rows) {
|
|
230
|
+
if (row.kind !== "connector-definition")
|
|
231
|
+
continue;
|
|
232
|
+
if (row.verb === "noop" || row.verb === "drift")
|
|
233
|
+
continue;
|
|
234
|
+
const def = row.desired;
|
|
235
|
+
if (!def)
|
|
236
|
+
continue;
|
|
237
|
+
const result = def.sourceCode !== undefined
|
|
238
|
+
? await client.installConnector({ sourceCode: def.sourceCode })
|
|
239
|
+
: await client.installConnector({ sourceUrl: def.sourceUrl });
|
|
240
|
+
if (result.connectorKey) {
|
|
241
|
+
locallySuppliedKeys.add(result.connectorKey);
|
|
242
|
+
installedKeys.add(result.connectorKey);
|
|
243
|
+
}
|
|
244
|
+
mutated = true;
|
|
245
|
+
printText(renderProgress(row.verb, "connector-definition", result.connectorKey || def.key || def.sourceFile, result.updated ? "(installed)" : "(unchanged)"));
|
|
246
|
+
}
|
|
247
|
+
// Bundled connectors referenced by an auth-profile / connection — installed
|
|
248
|
+
// ONLY if the org doesn't already have that key (installed in a prior apply
|
|
249
|
+
// or just installed from local source above). A locally-supplied key is never
|
|
250
|
+
// overwritten by the bundled `source_uri`.
|
|
251
|
+
const catalogByKey = new Map(catalog.filter((d) => d.installable && d.source_uri).map((d) => [d.key, d]));
|
|
252
|
+
const referenced = new Set([
|
|
253
|
+
...state.connectors.authProfiles.map((p) => p.connector),
|
|
254
|
+
...state.connectors.connections.map((c) => c.connector),
|
|
255
|
+
]);
|
|
256
|
+
for (const key of [...referenced].sort()) {
|
|
257
|
+
if (installedKeys.has(key) || locallySuppliedKeys.has(key))
|
|
258
|
+
continue;
|
|
259
|
+
const entry = catalogByKey.get(key);
|
|
260
|
+
if (!entry?.source_uri)
|
|
261
|
+
continue; // custom local-only — handled above
|
|
262
|
+
const result = await client.installConnector({
|
|
263
|
+
sourceUri: entry.source_uri,
|
|
264
|
+
});
|
|
265
|
+
mutated = true;
|
|
266
|
+
printText(renderProgress("create", "connector-definition", result.connectorKey || key, result.updated ? "(installed bundled)" : "(bundled — unchanged)"));
|
|
267
|
+
}
|
|
268
|
+
return mutated ? await client.listConnectorDefinitions(true) : catalog;
|
|
269
|
+
}
|
|
270
|
+
export function validateConnectorState(state, connectorDefinitions, opts = {}) {
|
|
271
|
+
const defByKey = new Map(connectorDefinitions.map((d) => [d.key, d]));
|
|
272
|
+
const authProfilesBySlug = new Map(state.connectors.authProfiles.map((p) => [p.slug, p]));
|
|
273
|
+
if (opts.requireInstalled) {
|
|
274
|
+
const refs = [
|
|
275
|
+
...state.connectors.authProfiles.map((p) => ({
|
|
276
|
+
connector: p.connector,
|
|
277
|
+
ref: `auth profile "${p.slug}"`,
|
|
278
|
+
})),
|
|
279
|
+
...state.connectors.connections.map((c) => ({
|
|
280
|
+
connector: c.connector,
|
|
281
|
+
ref: `connection "${c.slug}"`,
|
|
282
|
+
})),
|
|
283
|
+
];
|
|
284
|
+
for (const { connector, ref } of refs) {
|
|
285
|
+
const def = defByKey.get(connector);
|
|
286
|
+
if (!def || def.installed !== true) {
|
|
287
|
+
throw new ValidationError(`connector "${connector}" referenced by ${ref} is not installed in the org — check the \`connector\` key (and, for a local \`*.connector.ts\`, that its \`definition.key\` matches)`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const schemasFor = (connectorKey) => {
|
|
292
|
+
if (opts.skipSchemaForConnectorKeys?.has(connectorKey))
|
|
293
|
+
return null;
|
|
294
|
+
const def = defByKey.get(connectorKey);
|
|
295
|
+
return def ? resolveConnectorSchemas(def) : null;
|
|
296
|
+
};
|
|
297
|
+
for (const profile of state.connectors.authProfiles) {
|
|
298
|
+
validateAuthProfileAgainstConnector(profile, schemasFor(profile.connector));
|
|
299
|
+
}
|
|
300
|
+
for (const connection of state.connectors.connections) {
|
|
301
|
+
validateConnectionAgainstConnector(connection, authProfilesBySlug, schemasFor(connection.connector));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Connector keys declared locally (`*.connector.ts` / `type: connector`).
|
|
305
|
+
// We don't know the key for an auto-discovered `*.connector.ts` until the
|
|
306
|
+
// server compiles it — those have `key === null` — so they can't be in the
|
|
307
|
+
// skip set; their connections are validated only after install (when the key
|
|
308
|
+
// is known and the def is in the refreshed catalog).
|
|
309
|
+
export function locallyDeclaredConnectorKeys(state) {
|
|
310
|
+
return new Set(state.connectors.definitions
|
|
311
|
+
.map((d) => d.key)
|
|
312
|
+
.filter((k) => !!k));
|
|
313
|
+
}
|
|
314
|
+
async function executePlan(ctx, pendingAuth) {
|
|
55
315
|
const rowsByKind = (kind) => ctx.plan.rows.filter((row) => row.kind === kind && row.verb !== "noop" && row.verb !== "drift");
|
|
316
|
+
// 0) Connector definitions FIRST — install/update them (the plan was already
|
|
317
|
+
// confirmed), refetch the catalog, then re-validate connection/feed config
|
|
318
|
+
// against the now-current schemas. Doing this before any other resource
|
|
319
|
+
// means a post-install schema rejection halts apply before mutating
|
|
320
|
+
// anything unrelated.
|
|
321
|
+
const hasConnectorWork = ctx.state.connectors.definitions.length > 0 ||
|
|
322
|
+
ctx.state.connectors.authProfiles.length > 0 ||
|
|
323
|
+
ctx.state.connectors.connections.length > 0;
|
|
324
|
+
if (hasConnectorWork) {
|
|
325
|
+
const freshCatalog = await installConnectorDefinitions(ctx.client, ctx.state, ctx.remote.connectorDefinitions, ctx.plan);
|
|
326
|
+
validateConnectorState(ctx.state, freshCatalog, { requireInstalled: true });
|
|
327
|
+
}
|
|
56
328
|
// 1) Agents
|
|
57
329
|
for (const row of rowsByKind("agent")) {
|
|
58
330
|
if (row.kind !== "agent")
|
|
@@ -102,6 +374,21 @@ async function executePlan(ctx) {
|
|
|
102
374
|
: undefined;
|
|
103
375
|
printText(renderProgress(row.verb, "platform", `${row.agentId}/${row.id}`, detail));
|
|
104
376
|
}
|
|
377
|
+
// 3b) Declarative channel bindings — reconcile after the platform upserts
|
|
378
|
+
// above so the connection rows exist. Runs for every agent/platform that
|
|
379
|
+
// declares `channels` (the server reconcile is idempotent), independent of
|
|
380
|
+
// whether the platform's config changed in this plan.
|
|
381
|
+
for (const agent of ctx.state.agents) {
|
|
382
|
+
for (const platform of agent.platforms) {
|
|
383
|
+
if (!platform.channels || platform.channels.length === 0)
|
|
384
|
+
continue;
|
|
385
|
+
const res = await ctx.client.syncPlatformChannels(agent.metadata.agentId, platform.stableId, platform.channels);
|
|
386
|
+
const detail = res.removed.length > 0
|
|
387
|
+
? `(${res.bound.length} bound, ${res.removed.length} unbound)`
|
|
388
|
+
: `(${res.bound.length} bound)`;
|
|
389
|
+
printText(` ${chalk.cyan("↻")} ${chalk.bold("channels")} ${agent.metadata.agentId}/${platform.stableId} ${chalk.dim(detail)}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
105
392
|
// 4) Entity types
|
|
106
393
|
for (const row of rowsByKind("entity-type")) {
|
|
107
394
|
if (row.kind !== "entity-type")
|
|
@@ -120,11 +407,174 @@ async function executePlan(ctx) {
|
|
|
120
407
|
await ctx.client.upsertRelationshipType(row.desired);
|
|
121
408
|
printText(renderProgress(row.verb, "relationship-type", row.id));
|
|
122
409
|
}
|
|
410
|
+
// 6) Watchers (create-only; drift ignored)
|
|
411
|
+
for (const row of rowsByKind("watcher")) {
|
|
412
|
+
if (row.kind !== "watcher")
|
|
413
|
+
continue;
|
|
414
|
+
if (!row.desired)
|
|
415
|
+
continue;
|
|
416
|
+
const w = row.desired;
|
|
417
|
+
await ctx.client.createWatcher({
|
|
418
|
+
slug: w.slug,
|
|
419
|
+
agentId: w.agent,
|
|
420
|
+
name: w.name,
|
|
421
|
+
description: w.description,
|
|
422
|
+
prompt: w.prompt,
|
|
423
|
+
extraction_schema: w.extractionSchema,
|
|
424
|
+
schedule: w.schedule,
|
|
425
|
+
sources: w.sources,
|
|
426
|
+
});
|
|
427
|
+
printText(renderProgress(row.verb, "watcher", row.id));
|
|
428
|
+
}
|
|
429
|
+
// Auth profiles (create / update; interactive kinds → punch-list)
|
|
430
|
+
for (const row of rowsByKind("auth-profile")) {
|
|
431
|
+
if (row.kind !== "auth-profile")
|
|
432
|
+
continue;
|
|
433
|
+
const desired = ctx.state.connectors.authProfiles.find((p) => p.slug === row.id);
|
|
434
|
+
if (!desired)
|
|
435
|
+
continue;
|
|
436
|
+
const result = row.verb === "create"
|
|
437
|
+
? await ctx.client.createAuthProfile({
|
|
438
|
+
slug: desired.slug,
|
|
439
|
+
connector: desired.connector,
|
|
440
|
+
kind: desired.kind,
|
|
441
|
+
name: desired.name,
|
|
442
|
+
credentials: desired.credentials,
|
|
443
|
+
})
|
|
444
|
+
: await ctx.client.updateAuthProfile({
|
|
445
|
+
slug: desired.slug,
|
|
446
|
+
name: desired.name,
|
|
447
|
+
credentials: desired.credentials,
|
|
448
|
+
});
|
|
449
|
+
if ((desired.kind === "oauth_account" ||
|
|
450
|
+
desired.kind === "browser_session") &&
|
|
451
|
+
result.status !== "active") {
|
|
452
|
+
pendingAuth.push({
|
|
453
|
+
slug: desired.slug,
|
|
454
|
+
kind: desired.kind,
|
|
455
|
+
...(result.connectUrl ? { connectUrl: result.connectUrl } : {}),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
printText(renderProgress(row.verb, "auth-profile", row.id));
|
|
459
|
+
}
|
|
460
|
+
// 9) Connections, keyed by slug.
|
|
461
|
+
const remoteConnBySlug = new Map(ctx.remote.connections.map((c) => [c.slug, c]));
|
|
462
|
+
const connectionIdBySlug = new Map(ctx.remote.connections.map((c) => [c.slug, c.id]));
|
|
463
|
+
for (const row of rowsByKind("connection")) {
|
|
464
|
+
if (row.kind !== "connection")
|
|
465
|
+
continue;
|
|
466
|
+
const desired = ctx.state.connectors.connections.find((c) => c.slug === row.id);
|
|
467
|
+
if (!desired)
|
|
468
|
+
continue;
|
|
469
|
+
const existing = remoteConnBySlug.get(desired.slug);
|
|
470
|
+
if (existing && row.verb === "update") {
|
|
471
|
+
const updated = await ctx.client.updateConnection(existing.id, {
|
|
472
|
+
name: desired.name,
|
|
473
|
+
authProfileSlug: desired.authProfileSlug ?? null,
|
|
474
|
+
appAuthProfileSlug: desired.appAuthProfileSlug ?? null,
|
|
475
|
+
config: desired.config ?? {},
|
|
476
|
+
});
|
|
477
|
+
connectionIdBySlug.set(desired.slug, updated.id);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
const created = await ctx.client.createConnection({
|
|
481
|
+
slug: desired.slug,
|
|
482
|
+
connector: desired.connector,
|
|
483
|
+
name: desired.name,
|
|
484
|
+
authProfileSlug: desired.authProfileSlug,
|
|
485
|
+
appAuthProfileSlug: desired.appAuthProfileSlug,
|
|
486
|
+
config: desired.config,
|
|
487
|
+
});
|
|
488
|
+
connectionIdBySlug.set(desired.slug, created.id);
|
|
489
|
+
}
|
|
490
|
+
printText(renderProgress(row.verb, "connection", row.id));
|
|
491
|
+
}
|
|
492
|
+
// 10) Feeds (per connection — covers feeds whose connection itself was a noop)
|
|
493
|
+
for (const row of rowsByKind("feed")) {
|
|
494
|
+
if (row.kind !== "feed")
|
|
495
|
+
continue;
|
|
496
|
+
if (!row.desired)
|
|
497
|
+
continue;
|
|
498
|
+
const feed = row.desired;
|
|
499
|
+
const connectionId = connectionIdBySlug.get(row.connectionSlug);
|
|
500
|
+
if (connectionId === undefined) {
|
|
501
|
+
throw new ApiError(`feed "${feed.feedKey}" references connection "${row.connectionSlug}" which has no remote ID — connection create may have failed`);
|
|
502
|
+
}
|
|
503
|
+
const existingConn = remoteConnBySlug.get(row.connectionSlug);
|
|
504
|
+
const remoteFeed = existingConn
|
|
505
|
+
? (ctx.remote.feedsByConnectionId.get(existingConn.id) ?? []).find((f) => f.feed_key === feed.feedKey)
|
|
506
|
+
: undefined;
|
|
507
|
+
if (remoteFeed && row.verb === "update") {
|
|
508
|
+
await ctx.client.updateFeed(remoteFeed.id, {
|
|
509
|
+
name: feed.name,
|
|
510
|
+
schedule: feed.schedule,
|
|
511
|
+
config: feed.config ?? {},
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
await ctx.client.createFeed({
|
|
516
|
+
connectionId,
|
|
517
|
+
feedKey: feed.feedKey,
|
|
518
|
+
name: feed.name,
|
|
519
|
+
schedule: feed.schedule,
|
|
520
|
+
config: feed.config,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
printText(renderProgress(row.verb, "feed", row.id));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Collect pending interactive-auth profiles from a (no-op) plan and re-issue a
|
|
527
|
+
// fresh connect URL — used both when "nothing to apply" and on partial failure.
|
|
528
|
+
async function collectPendingAuthFromPlan(client, plan, already) {
|
|
529
|
+
const out = [...already];
|
|
530
|
+
for (const row of plan.rows) {
|
|
531
|
+
if (row.kind !== "auth-profile" || !("needsAuth" in row) || !row.needsAuth)
|
|
532
|
+
continue;
|
|
533
|
+
if (!row.desired)
|
|
534
|
+
continue;
|
|
535
|
+
const desired = row.desired;
|
|
536
|
+
if (out.some((p) => p.slug === desired.slug))
|
|
537
|
+
continue;
|
|
538
|
+
if (desired.kind === "oauth_account") {
|
|
539
|
+
// A successful reconnect implies the profile exists remotely (and yields
|
|
540
|
+
// a fresh connect URL). If it fails, the profile may not exist (a failed
|
|
541
|
+
// create in a partial apply) — don't tell the operator to go finish auth
|
|
542
|
+
// for something that isn't there; just skip it.
|
|
543
|
+
const connectUrl = await client
|
|
544
|
+
.reconnectAuthProfile(desired.slug)
|
|
545
|
+
.catch(() => undefined);
|
|
546
|
+
if (!connectUrl)
|
|
547
|
+
continue;
|
|
548
|
+
out.push({ slug: desired.slug, kind: desired.kind, connectUrl });
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
// browser_session (no reconnect endpoint): include only if the profile row
|
|
552
|
+
// actually exists remotely.
|
|
553
|
+
const exists = await client
|
|
554
|
+
.getAuthProfileBySlug(desired.slug)
|
|
555
|
+
.catch(() => null);
|
|
556
|
+
if (!exists)
|
|
557
|
+
continue;
|
|
558
|
+
out.push({ slug: desired.slug, kind: desired.kind });
|
|
559
|
+
}
|
|
560
|
+
return out;
|
|
123
561
|
}
|
|
124
562
|
// ── Top-level command ──────────────────────────────────────────────────────
|
|
563
|
+
/** "office-bot" → "Office Bot" — default display name for a bootstrapped org. */
|
|
564
|
+
function slugToTitle(slug) {
|
|
565
|
+
return (slug
|
|
566
|
+
.split(/[-_]+/)
|
|
567
|
+
.filter(Boolean)
|
|
568
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
569
|
+
.join(" ") || slug);
|
|
570
|
+
}
|
|
125
571
|
export async function applyCommand(opts = {}) {
|
|
126
572
|
const cwd = opts.cwd ?? process.cwd();
|
|
127
|
-
const
|
|
573
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
574
|
+
const { state, configPath } = await loadDesiredState({
|
|
575
|
+
cwd,
|
|
576
|
+
...(opts.only ? { only: opts.only } : {}),
|
|
577
|
+
});
|
|
128
578
|
printText(chalk.dim(`Config: ${configPath}`));
|
|
129
579
|
// Required secrets gate: fail before any network mutation.
|
|
130
580
|
const { missing } = checkRequiredSecrets(state);
|
|
@@ -132,27 +582,104 @@ export async function applyCommand(opts = {}) {
|
|
|
132
582
|
printError(renderMissingSecrets(missing));
|
|
133
583
|
throw new ValidationError(`${missing.length} required secret${missing.length === 1 ? "" : "s"} missing — see above.`);
|
|
134
584
|
}
|
|
135
|
-
|
|
585
|
+
// Org slug resolution: explicit --org ▸ active-session org ▸ `[memory].org`
|
|
586
|
+
// from lobu.toml. The toml slug is the declarative default — if no org with
|
|
587
|
+
// that slug exists yet, `lobu apply` offers to provision it (below).
|
|
588
|
+
const { client, orgSlug, apiBaseUrl } = await resolveApplyClient({
|
|
136
589
|
url: opts.url,
|
|
137
|
-
org: opts.org,
|
|
590
|
+
org: opts.org ?? state.memory?.org,
|
|
138
591
|
fetchImpl: opts.fetchImpl,
|
|
139
592
|
});
|
|
140
593
|
printText(chalk.dim(`Org: ${orgSlug}`));
|
|
594
|
+
// Refuse if .lobu/project.json points at a different (context, org).
|
|
595
|
+
const link = await loadProjectLink(cwd);
|
|
596
|
+
if (link && !opts.force) {
|
|
597
|
+
const activeContext = await resolveContext().catch(() => null);
|
|
598
|
+
const contextMismatch = activeContext !== null && activeContext.name !== link.context;
|
|
599
|
+
const orgMismatch = orgSlug !== link.org;
|
|
600
|
+
if (contextMismatch || orgMismatch) {
|
|
601
|
+
const detail = [];
|
|
602
|
+
if (contextMismatch) {
|
|
603
|
+
detail.push(` context: linked=${link.context}, active=${activeContext.name}`);
|
|
604
|
+
}
|
|
605
|
+
if (orgMismatch) {
|
|
606
|
+
detail.push(` org: linked=${link.org}, applying=${orgSlug}`);
|
|
607
|
+
}
|
|
608
|
+
printError([
|
|
609
|
+
"",
|
|
610
|
+
"Project link mismatch — refusing to apply.",
|
|
611
|
+
...detail,
|
|
612
|
+
"",
|
|
613
|
+
"Run `lobu link --org <slug>` to update the link, or pass `--force` to override.",
|
|
614
|
+
].join("\n"));
|
|
615
|
+
throw new ValidationError("project-link mismatch");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// Check the resolved org exists / the operator is a member. `lobu apply`
|
|
619
|
+
// can't create an org headlessly — that needs a logged-in browser session —
|
|
620
|
+
// so a missing org stops here with a link to create it. `listOrgs()` failing
|
|
621
|
+
// (old server, or a token the userinfo endpoint rejects) → null → skip the
|
|
622
|
+
// check and let the normal flow surface any org error.
|
|
623
|
+
const myOrgs = await client.listOrgs().catch(() => null);
|
|
624
|
+
const resolvedOrg = myOrgs?.find((o) => o.slug === orgSlug) ??
|
|
625
|
+
(state.memory?.organizationId
|
|
626
|
+
? myOrgs?.find((o) => o.id === state.memory?.organizationId)
|
|
627
|
+
: undefined);
|
|
628
|
+
if (myOrgs !== null && !resolvedOrg) {
|
|
629
|
+
const orgName = state.memory?.name ?? slugToTitle(orgSlug);
|
|
630
|
+
const createUrl = `${apiBaseUrl}/orgs/new?slug=${encodeURIComponent(orgSlug)}&name=${encodeURIComponent(orgName)}`;
|
|
631
|
+
printError([
|
|
632
|
+
"",
|
|
633
|
+
`Organization "${orgSlug}" not found, or you're not a member.`,
|
|
634
|
+
"",
|
|
635
|
+
` Create it: ${createUrl}`,
|
|
636
|
+
` (or: \`lobu org create ${orgSlug}\`)`,
|
|
637
|
+
"",
|
|
638
|
+
"then re-run `lobu apply`. (Or target an existing org with `--org <slug>`.)",
|
|
639
|
+
].join("\n"));
|
|
640
|
+
throw new ValidationError(`organization "${orgSlug}" not found`);
|
|
641
|
+
}
|
|
642
|
+
// Persist the resolved org id back into lobu.toml so the whole team applies
|
|
643
|
+
// to the same org. Best-effort — a read-only lobu.toml must not fail apply.
|
|
644
|
+
// Skipped on `--dry-run`: that flag promises no mutation, local files included.
|
|
645
|
+
if (!opts.dryRun &&
|
|
646
|
+
resolvedOrg &&
|
|
647
|
+
state.memory?.organizationId !== resolvedOrg.id) {
|
|
648
|
+
await writeMemoryOrganizationId(cwd, resolvedOrg.id).catch(() => undefined);
|
|
649
|
+
}
|
|
650
|
+
// SECURITY (#4): confirm BEFORE fetching any `source_url` or uploading custom
|
|
651
|
+
// connector source — `lobu apply --dry-run` should never hit a manifest URL.
|
|
652
|
+
if (!opts.dryRun) {
|
|
653
|
+
await confirmCustomConnectorSource(state.connectors.definitions, opts.yes ?? false);
|
|
654
|
+
await materializeConnectorSource(state.connectors.definitions, fetchImpl);
|
|
655
|
+
}
|
|
656
|
+
// Snapshot remote state. Connector-def rows in the plan are computed against
|
|
657
|
+
// this (current/stale) catalog — "create" when the key isn't installed,
|
|
658
|
+
// "update" when it is. Connector defs are NOT installed here; that happens in
|
|
659
|
+
// `executePlan`, AFTER plan confirmation.
|
|
141
660
|
const remote = await fetchRemoteSnapshot(client, state, opts.only);
|
|
661
|
+
// Validate connection/auth-profile config against the catalog we have now,
|
|
662
|
+
// but SKIP schema validation for connector keys declared locally — those
|
|
663
|
+
// might update an already-installed schema in this same apply, so they're
|
|
664
|
+
// schema-validated later (post-install, against the fresh catalog) inside
|
|
665
|
+
// `executePlan`. Structural checks (auth-slug existence, connector match)
|
|
666
|
+
// still run here for every connection.
|
|
667
|
+
validateConnectorState(state, remote.connectorDefinitions, {
|
|
668
|
+
skipSchemaForConnectorKeys: locallyDeclaredConnectorKeys(state),
|
|
669
|
+
});
|
|
142
670
|
const plan = computeDiff(state, remote, { only: opts.only });
|
|
143
671
|
printText(renderPlan(plan));
|
|
144
672
|
if (opts.dryRun) {
|
|
145
|
-
printText(chalk.dim("\nDry run — no changes applied."));
|
|
673
|
+
printText(chalk.dim("\nDry run — no changes applied. (Connector-definition install + post-install schema validation are skipped in dry-run.)"));
|
|
146
674
|
return;
|
|
147
675
|
}
|
|
148
|
-
|
|
676
|
+
const hasPendingAuth = plan.rows.some((r) => r.kind === "auth-profile" && "needsAuth" in r && r.needsAuth);
|
|
677
|
+
if (plan.counts.create === 0 && plan.counts.update === 0 && !hasPendingAuth) {
|
|
149
678
|
printText(chalk.green("\nNothing to apply."));
|
|
150
679
|
return;
|
|
151
680
|
}
|
|
152
|
-
// Build a plain-text summary for the inquirer prompt — chalk-decorated
|
|
153
|
-
// text confuses some terminals when re-printed by the prompt library.
|
|
154
681
|
const { create, update, noop, drift } = plan.counts;
|
|
155
|
-
const summaryLine = `${create} create, ${update} update, ${noop} noop, ${drift} drift`;
|
|
682
|
+
const summaryLine = `${create} create, ${update} update, ${noop} noop, ${drift} drift${hasPendingAuth ? " + pending auth" : ""}`;
|
|
156
683
|
const approved = await confirmPlan({
|
|
157
684
|
yes: opts.yes ?? false,
|
|
158
685
|
summaryLine,
|
|
@@ -161,23 +688,30 @@ export async function applyCommand(opts = {}) {
|
|
|
161
688
|
printText(chalk.dim("\nCancelled."));
|
|
162
689
|
return;
|
|
163
690
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
printText(chalk.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
printError(`\n${err.message}`);
|
|
691
|
+
const pendingAuth = [];
|
|
692
|
+
let applyErr;
|
|
693
|
+
if (plan.counts.create > 0 || plan.counts.update > 0) {
|
|
694
|
+
printText(chalk.bold("\nApplying:"));
|
|
695
|
+
try {
|
|
696
|
+
await executePlan({ client, state, plan, remote }, pendingAuth);
|
|
697
|
+
printText(chalk.green("\nApply complete."));
|
|
172
698
|
}
|
|
173
|
-
|
|
174
|
-
|
|
699
|
+
catch (err) {
|
|
700
|
+
applyErr = err;
|
|
701
|
+
printError(`\n${err instanceof Error ? err.message : String(err)}`);
|
|
702
|
+
printError("Apply halted on first failure. Re-run `lobu apply` once the underlying issue is resolved — every endpoint is idempotent.");
|
|
175
703
|
}
|
|
176
|
-
else {
|
|
177
|
-
printError(`\n${String(err)}`);
|
|
178
|
-
}
|
|
179
|
-
printError("Apply halted on first failure. Re-run `lobu apply` once the underlying issue is resolved — every endpoint is idempotent.");
|
|
180
|
-
throw err;
|
|
181
704
|
}
|
|
705
|
+
// Always render the punch-list — even on partial failure, so the operator
|
|
706
|
+
// keeps the connect URLs and the informational notes.
|
|
707
|
+
const finalPending = await collectPendingAuthFromPlan(client, plan, pendingAuth);
|
|
708
|
+
const punchList = renderPostApplyPunchList({
|
|
709
|
+
pendingAuth: finalPending,
|
|
710
|
+
notes: plan.notes,
|
|
711
|
+
});
|
|
712
|
+
if (punchList)
|
|
713
|
+
printText(punchList);
|
|
714
|
+
if (applyErr)
|
|
715
|
+
throw applyErr;
|
|
182
716
|
}
|
|
183
717
|
//# sourceMappingURL=apply-cmd.js.map
|