@elevasis/sdk 0.4.0 → 0.4.1

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/dist/cli.cjs CHANGED
@@ -39750,11 +39750,12 @@ var ResourceRegistry = class {
39750
39750
  */
39751
39751
  serializedCache;
39752
39752
  /**
39753
- * Runtime-registered organizations (external deployments)
39754
- * Tracks which orgs were added at runtime vs. static startup.
39755
- * Used to guard against unregistering static orgs and to store remote config.
39753
+ * Per-resource remote configuration (external deployments)
39754
+ * Key: "orgName/resourceId", Value: RemoteOrgConfig for that resource.
39755
+ * Tracks which individual resources were added at runtime via deploy pipeline.
39756
+ * Static and remote resources coexist in the same org.
39756
39757
  */
39757
- runtimeOrgs = /* @__PURE__ */ new Map();
39758
+ remoteResources = /* @__PURE__ */ new Map();
39758
39759
  /**
39759
39760
  * Validates registry on construction
39760
39761
  * - Checks for duplicate resourceIds within organizations
@@ -39857,67 +39858,125 @@ var ResourceRegistry = class {
39857
39858
  // Runtime Organization Registration (External Deployments)
39858
39859
  // ============================================================================
39859
39860
  /**
39860
- * Register an external organization at runtime
39861
+ * Register external resources at runtime
39861
39862
  *
39862
39863
  * Called during deploy pipeline when an external developer deploys their bundle.
39863
- * Adds the org's stub definitions to the registry and stores remote config
39864
- * for worker thread execution branching.
39864
+ * Merges the incoming stub definitions into the org's registry and stores
39865
+ * per-resource remote config for worker thread execution branching.
39865
39866
  *
39866
- * - If orgName conflicts with a static (startup) org, throws an error
39867
- * - If orgName is already runtime-registered (redeploy), unregisters first
39867
+ * Static and remote resources coexist in the same org. If the org already
39868
+ * has static resources, the incoming remote resources are merged alongside them.
39869
+ * If redeploying (some resources already registered as remote for this org),
39870
+ * the previous remote resources are unregistered first.
39868
39871
  *
39869
39872
  * @param orgName - Organization name (used as registry key)
39870
39873
  * @param org - Stub resource definitions (workflows/agents with placeholder handlers)
39871
39874
  * @param remote - Remote configuration (bundle path, deployment ID, env vars)
39872
- * @throws Error if orgName conflicts with a static organization
39875
+ * @throws Error if incoming resourceId conflicts with a static resource
39876
+ * @throws Error if incoming deployment contains duplicate resourceIds
39873
39877
  */
39874
39878
  registerOrganization(orgName, org, remote) {
39875
- if (this.registry[orgName] && !this.runtimeOrgs.has(orgName)) {
39876
- throw new Error(`Organization '${orgName}' conflicts with a static organization`);
39879
+ const incomingWorkflowIds = (org.workflows ?? []).map((w) => w.config.resourceId);
39880
+ const incomingAgentIds = (org.agents ?? []).map((a) => a.config.resourceId);
39881
+ const incomingIds = [...incomingWorkflowIds, ...incomingAgentIds];
39882
+ const seen = /* @__PURE__ */ new Set();
39883
+ for (const id of incomingIds) {
39884
+ if (seen.has(id)) {
39885
+ throw new Error(
39886
+ `Duplicate resource ID '${id}' in deployment. Each resource must have a unique ID.`
39887
+ );
39888
+ }
39889
+ seen.add(id);
39877
39890
  }
39878
- if (this.runtimeOrgs.has(orgName)) {
39891
+ if (this.isRemote(orgName)) {
39879
39892
  this.unregisterOrganization(orgName);
39880
39893
  }
39881
- this.registry[orgName] = org;
39882
- this.runtimeOrgs.set(orgName, remote);
39883
- this.serializedCache.set(orgName, serializeOrganization(org));
39894
+ const existingOrg = this.registry[orgName];
39895
+ if (existingOrg) {
39896
+ const staticWorkflowIds = new Set((existingOrg.workflows ?? []).map((w) => w.config.resourceId));
39897
+ const staticAgentIds = new Set((existingOrg.agents ?? []).map((a) => a.config.resourceId));
39898
+ for (const id of incomingIds) {
39899
+ if (staticWorkflowIds.has(id) || staticAgentIds.has(id)) {
39900
+ throw new Error(
39901
+ `Resource '${id}' already exists in '${orgName}' as an internal resource. External deployments cannot override internal resources.`
39902
+ );
39903
+ }
39904
+ }
39905
+ }
39906
+ if (existingOrg) {
39907
+ existingOrg.workflows = [...existingOrg.workflows ?? [], ...org.workflows ?? []];
39908
+ existingOrg.agents = [...existingOrg.agents ?? [], ...org.agents ?? []];
39909
+ } else {
39910
+ this.registry[orgName] = org;
39911
+ }
39912
+ for (const id of incomingIds) {
39913
+ this.remoteResources.set(`${orgName}/${id}`, remote);
39914
+ }
39915
+ this.serializedCache.set(orgName, serializeOrganization(this.registry[orgName]));
39884
39916
  }
39885
39917
  /**
39886
- * Unregister a runtime-registered organization
39918
+ * Unregister runtime-registered resources for an organization
39887
39919
  *
39888
- * Only removes organizations that were registered at runtime (via registerOrganization).
39889
- * Static organizations loaded at startup are protected and cannot be unregistered.
39890
- * No-op if the org is not runtime-registered.
39920
+ * Removes only resources that were registered at runtime (via registerOrganization).
39921
+ * Static resources loaded at startup are preserved. If the org still has static
39922
+ * resources after removal, the serialization cache is rebuilt. If no resources
39923
+ * remain, the org is fully removed from the registry.
39924
+ * No-op if the org has no remote resources.
39891
39925
  *
39892
- * @param orgName - Organization name to unregister
39926
+ * @param orgName - Organization name to unregister remote resources from
39893
39927
  */
39894
39928
  unregisterOrganization(orgName) {
39895
- if (!this.runtimeOrgs.has(orgName)) return;
39896
- delete this.registry[orgName];
39897
- this.runtimeOrgs.delete(orgName);
39898
- this.serializedCache.delete(orgName);
39929
+ const prefix = `${orgName}/`;
39930
+ const remoteIds = /* @__PURE__ */ new Set();
39931
+ for (const key of this.remoteResources.keys()) {
39932
+ if (key.startsWith(prefix)) {
39933
+ remoteIds.add(key.slice(prefix.length));
39934
+ this.remoteResources.delete(key);
39935
+ }
39936
+ }
39937
+ if (remoteIds.size === 0) return;
39938
+ const orgResources = this.registry[orgName];
39939
+ if (!orgResources) return;
39940
+ orgResources.workflows = (orgResources.workflows ?? []).filter(
39941
+ (w) => !remoteIds.has(w.config.resourceId)
39942
+ );
39943
+ orgResources.agents = (orgResources.agents ?? []).filter(
39944
+ (a) => !remoteIds.has(a.config.resourceId)
39945
+ );
39946
+ const remaining = (orgResources.workflows?.length ?? 0) + (orgResources.agents?.length ?? 0) + (orgResources.triggers?.length ?? 0) + (orgResources.integrations?.length ?? 0) + (orgResources.externalResources?.length ?? 0) + (orgResources.humanCheckpoints?.length ?? 0);
39947
+ if (remaining > 0) {
39948
+ this.serializedCache.set(orgName, serializeOrganization(orgResources));
39949
+ } else {
39950
+ delete this.registry[orgName];
39951
+ this.serializedCache.delete(orgName);
39952
+ }
39899
39953
  }
39900
39954
  /**
39901
- * Get remote configuration for an organization
39955
+ * Get remote configuration for a specific resource
39902
39956
  *
39903
- * Returns the RemoteOrgConfig if the org was registered at runtime,
39904
- * or null if it's a static org or doesn't exist.
39957
+ * Returns the RemoteOrgConfig if the resource was registered at runtime,
39958
+ * or null if it's a static resource or doesn't exist.
39905
39959
  * Used by the execution coordinator to branch between local and worker execution.
39906
39960
  *
39907
39961
  * @param orgName - Organization name
39962
+ * @param resourceId - Resource ID
39908
39963
  * @returns Remote config or null
39909
39964
  */
39910
- getRemoteConfig(orgName) {
39911
- return this.runtimeOrgs.get(orgName) ?? null;
39965
+ getRemoteConfig(orgName, resourceId) {
39966
+ return this.remoteResources.get(`${orgName}/${resourceId}`) ?? null;
39912
39967
  }
39913
39968
  /**
39914
- * Check if an organization is a remote (externally deployed) organization
39969
+ * Check if an organization has any remote (externally deployed) resources
39915
39970
  *
39916
39971
  * @param orgName - Organization name
39917
- * @returns true if the org was registered at runtime
39972
+ * @returns true if the org has at least one runtime-registered resource
39918
39973
  */
39919
39974
  isRemote(orgName) {
39920
- return this.runtimeOrgs.has(orgName);
39975
+ const prefix = `${orgName}/`;
39976
+ for (const key of this.remoteResources.keys()) {
39977
+ if (key.startsWith(prefix)) return true;
39978
+ }
39979
+ return false;
39921
39980
  }
39922
39981
  // ============================================================================
39923
39982
  // Resource Manifest Accessors
@@ -40116,20 +40175,11 @@ function wrapAction(commandName, fn) {
40116
40175
  // src/cli/commands/check.ts
40117
40176
  var import_meta = {};
40118
40177
  function registerCheckCommand(program3) {
40119
- program3.command("check").description("Validate project resources against the ResourceRegistry\n Example: elevasis check --config ./elevasis.config.ts --entry ./src/index.ts").option("--config <path>", "Path to elevasis.config.ts (default: ./elevasis.config.ts)").option("--entry <path>", "Path to entry file (default: ./src/index.ts)").action(wrapAction("check", async (options) => {
40120
- const configPath = options.config ?? "./elevasis.config.ts";
40178
+ program3.command("check").description("Validate project resources against the ResourceRegistry\n Example: elevasis check --entry ./src/index.ts").option("--entry <path>", "Path to entry file (default: ./src/index.ts)").action(wrapAction("check", async (options) => {
40121
40179
  const entryPath = options.entry ?? "./src/index.ts";
40122
40180
  const spinner = ora("Validating resources...").start();
40123
40181
  try {
40124
40182
  const jiti = (0, import_jiti.createJiti)(import_meta.url);
40125
- const configModule = await jiti.import((0, import_path.resolve)(configPath));
40126
- const config3 = configModule.default;
40127
- if (!config3?.organization) {
40128
- spinner.fail('Invalid config: missing "organization" field');
40129
- console.error(source_default.gray(` Config file: ${(0, import_path.resolve)(configPath)}`));
40130
- console.error(source_default.gray(' Expected: export default { organization: "your-org-name" }'));
40131
- throw new Error("Invalid config");
40132
- }
40133
40183
  const entryModule = await jiti.import((0, import_path.resolve)(entryPath));
40134
40184
  const org = entryModule.default;
40135
40185
  if (!org) {
@@ -40138,7 +40188,7 @@ function registerCheckCommand(program3) {
40138
40188
  console.error(source_default.gray(" Expected: export default { workflows: [...], agents: [...] }"));
40139
40189
  throw new Error("Invalid entry");
40140
40190
  }
40141
- new ResourceRegistry({ [config3.organization]: org });
40191
+ new ResourceRegistry({ _check: org });
40142
40192
  const workflowCount = org.workflows?.length ?? 0;
40143
40193
  const agentCount = org.agents?.length ?? 0;
40144
40194
  const totalCount = workflowCount + agentCount;
@@ -40189,27 +40239,73 @@ function resolveEnvironment() {
40189
40239
  return process.env.NODE_ENV === "development" ? "development" : "production";
40190
40240
  }
40191
40241
 
40242
+ // src/cli/api-client.ts
40243
+ function getApiKey() {
40244
+ const key = process.env.ELEVASIS_API_KEY;
40245
+ if (!key) {
40246
+ throw new Error(
40247
+ "ELEVASIS_API_KEY environment variable is required.\nSet it in your .env file: ELEVASIS_API_KEY=sk_..."
40248
+ );
40249
+ }
40250
+ return key;
40251
+ }
40252
+ async function apiGet(endpoint, apiUrl = resolveApiUrl()) {
40253
+ const response = await fetch(`${apiUrl}${endpoint}`, {
40254
+ headers: { Authorization: `Bearer ${getApiKey()}` }
40255
+ });
40256
+ if (!response.ok) {
40257
+ const errorText = await response.text();
40258
+ throw new Error(`API request failed (${response.status}): ${errorText}`);
40259
+ }
40260
+ if (response.status === 204) {
40261
+ return {};
40262
+ }
40263
+ return response.json();
40264
+ }
40265
+ async function apiPost(endpoint, body, apiUrl = resolveApiUrl()) {
40266
+ const response = await fetch(`${apiUrl}${endpoint}`, {
40267
+ method: "POST",
40268
+ headers: {
40269
+ Authorization: `Bearer ${getApiKey()}`,
40270
+ "Content-Type": "application/json"
40271
+ },
40272
+ body: JSON.stringify(body)
40273
+ });
40274
+ if (!response.ok) {
40275
+ const errorText = await response.text();
40276
+ throw new Error(`API request failed (${response.status}): ${errorText}`);
40277
+ }
40278
+ if (response.status === 204) {
40279
+ return {};
40280
+ }
40281
+ return response.json();
40282
+ }
40283
+
40192
40284
  // src/cli/commands/deploy.ts
40193
40285
  var import_meta2 = {};
40194
40286
  function registerDeployCommand(program3) {
40195
- program3.command("deploy").description("Validate, bundle, upload, and deploy project resources\n Example: elevasis deploy --api-url http://localhost:5170").option("--api-url <url>", "API URL").option("--config <path>", "Path to elevasis.config.ts (default: ./elevasis.config.ts)").option("--entry <path>", "Path to entry file (default: ./src/index.ts)").action(wrapAction("deploy", async (options) => {
40287
+ program3.command("deploy").description("Validate, bundle, upload, and deploy project resources\n Example: elevasis deploy --api-url http://localhost:5170").option("--api-url <url>", "API URL").option("--entry <path>", "Path to entry file (default: ./src/index.ts)").action(wrapAction("deploy", async (options) => {
40196
40288
  const startTime = Date.now();
40197
40289
  const apiUrl = resolveApiUrl(options.apiUrl);
40198
40290
  const env2 = resolveEnvironment();
40199
- const configPath = options.config ?? "./elevasis.config.ts";
40200
40291
  const entryPath = options.entry ?? "./src/index.ts";
40292
+ const authSpinner = ora("Authenticating...").start();
40293
+ let orgName;
40294
+ try {
40295
+ const me = await apiGet("/api/external/me", apiUrl);
40296
+ orgName = me.organizationName;
40297
+ authSpinner.succeed(
40298
+ source_default.green("Authenticating...") + source_default.white(" done") + source_default.gray(` (${orgName})`)
40299
+ );
40300
+ } catch (error46) {
40301
+ authSpinner.fail(source_default.red("Authentication failed"));
40302
+ console.error(source_default.gray(" Check your ELEVASIS_API_KEY and API URL."));
40303
+ throw error46;
40304
+ }
40201
40305
  const validateSpinner = ora("Validating...").start();
40202
- let config3;
40203
40306
  let org;
40204
40307
  try {
40205
40308
  const jiti = (0, import_jiti2.createJiti)(import_meta2.url);
40206
- const configModule = await jiti.import((0, import_path2.resolve)(configPath));
40207
- config3 = configModule.default;
40208
- if (!config3?.organization) {
40209
- validateSpinner.fail('Invalid config: missing "organization" field');
40210
- console.error(source_default.gray(` Config file: ${(0, import_path2.resolve)(configPath)}`));
40211
- throw new Error('Invalid config: missing "organization" field');
40212
- }
40213
40309
  const entryModule = await jiti.import((0, import_path2.resolve)(entryPath));
40214
40310
  org = entryModule.default;
40215
40311
  if (!org) {
@@ -40217,7 +40313,7 @@ function registerDeployCommand(program3) {
40217
40313
  console.error(source_default.gray(` Entry file: ${(0, import_path2.resolve)(entryPath)}`));
40218
40314
  throw new Error("Invalid entry: no default export found");
40219
40315
  }
40220
- new ResourceRegistry({ [config3.organization]: org });
40316
+ new ResourceRegistry({ [orgName]: org });
40221
40317
  const workflowCount = org.workflows?.length ?? 0;
40222
40318
  const agentCount = org.agents?.length ?? 0;
40223
40319
  const totalCount = workflowCount + agentCount;
@@ -40225,7 +40321,7 @@ function registerDeployCommand(program3) {
40225
40321
  source_default.green("Validating...") + source_default.white(" done") + source_default.gray(` (${totalCount} resource${totalCount !== 1 ? "s" : ""}, 0 errors)`)
40226
40322
  );
40227
40323
  console.log("");
40228
- console.log(source_default.gray(` Org: ${config3.organization}`));
40324
+ console.log(source_default.gray(` Org: ${orgName}`));
40229
40325
  console.log(source_default.gray(` Target: ${apiUrl} (${env2})`));
40230
40326
  console.log("");
40231
40327
  for (const w of org.workflows ?? []) {
@@ -40401,48 +40497,6 @@ startWorker(org)
40401
40497
  }));
40402
40498
  }
40403
40499
 
40404
- // src/cli/api-client.ts
40405
- function getApiKey() {
40406
- const key = process.env.ELEVASIS_API_KEY;
40407
- if (!key) {
40408
- throw new Error(
40409
- "ELEVASIS_API_KEY environment variable is required.\nSet it in your .env file: ELEVASIS_API_KEY=sk_..."
40410
- );
40411
- }
40412
- return key;
40413
- }
40414
- async function apiGet(endpoint, apiUrl = resolveApiUrl()) {
40415
- const response = await fetch(`${apiUrl}${endpoint}`, {
40416
- headers: { Authorization: `Bearer ${getApiKey()}` }
40417
- });
40418
- if (!response.ok) {
40419
- const errorText = await response.text();
40420
- throw new Error(`API request failed (${response.status}): ${errorText}`);
40421
- }
40422
- if (response.status === 204) {
40423
- return {};
40424
- }
40425
- return response.json();
40426
- }
40427
- async function apiPost(endpoint, body, apiUrl = resolveApiUrl()) {
40428
- const response = await fetch(`${apiUrl}${endpoint}`, {
40429
- method: "POST",
40430
- headers: {
40431
- Authorization: `Bearer ${getApiKey()}`,
40432
- "Content-Type": "application/json"
40433
- },
40434
- body: JSON.stringify(body)
40435
- });
40436
- if (!response.ok) {
40437
- const errorText = await response.text();
40438
- throw new Error(`API request failed (${response.status}): ${errorText}`);
40439
- }
40440
- if (response.status === 204) {
40441
- return {};
40442
- }
40443
- return response.json();
40444
- }
40445
-
40446
40500
  // src/cli/commands/exec.ts
40447
40501
  function registerExecCommand(program3) {
40448
40502
  program3.command("exec <resourceId>").description(`Execute a deployed resource
@@ -40775,7 +40829,7 @@ var import_path3 = require("path");
40775
40829
  var import_promises2 = require("fs/promises");
40776
40830
 
40777
40831
  // src/cli/version.ts
40778
- var SDK_VERSION = "0.4.0";
40832
+ var SDK_VERSION = "0.4.1";
40779
40833
 
40780
40834
  // src/cli/commands/init.ts
40781
40835
  var SCAFFOLD_FILES = [
@@ -40812,7 +40866,7 @@ function registerInitCommand(program3) {
40812
40866
  }
40813
40867
  await (0, import_promises2.mkdir)((0, import_path3.resolve)(targetDir, "src"), { recursive: true });
40814
40868
  const files = {
40815
- "elevasis.config.ts": configTemplate(orgSlug),
40869
+ "elevasis.config.ts": configTemplate(),
40816
40870
  "package.json": packageJsonTemplate(orgSlug),
40817
40871
  "pnpm-workspace.yaml": pnpmWorkspaceTemplate(),
40818
40872
  "tsconfig.json": tsconfigTemplate(),
@@ -40840,12 +40894,10 @@ function toSlug(name) {
40840
40894
  const slug = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^[^a-z]+/, "").replace(/-+/g, "-").replace(/-$/, "");
40841
40895
  return slug.length >= 3 ? slug : "my-project";
40842
40896
  }
40843
- function configTemplate(organization) {
40897
+ function configTemplate() {
40844
40898
  return `import type { ElevasConfig } from '@elevasis/sdk'
40845
40899
 
40846
- export default {
40847
- organization: '${organization}',
40848
- } satisfies ElevasConfig
40900
+ export default {} satisfies ElevasConfig
40849
40901
  `;
40850
40902
  }
40851
40903
  function packageJsonTemplate(organization) {
@@ -40938,7 +40990,7 @@ elevasis executions <resourceId>
40938
40990
 
40939
40991
  ## Project Structure
40940
40992
 
40941
- - \`elevasis.config.ts\` -- Organization config
40993
+ - \`elevasis.config.ts\` -- Project config (optional settings)
40942
40994
  - \`src/index.ts\` -- Resource definitions (workflows, agents)
40943
40995
  - \`.env\` -- API key and environment variables
40944
40996
  `;
package/dist/index.d.ts CHANGED
@@ -1104,11 +1104,12 @@ declare class ResourceRegistry {
1104
1104
  */
1105
1105
  private serializedCache;
1106
1106
  /**
1107
- * Runtime-registered organizations (external deployments)
1108
- * Tracks which orgs were added at runtime vs. static startup.
1109
- * Used to guard against unregistering static orgs and to store remote config.
1107
+ * Per-resource remote configuration (external deployments)
1108
+ * Key: "orgName/resourceId", Value: RemoteOrgConfig for that resource.
1109
+ * Tracks which individual resources were added at runtime via deploy pipeline.
1110
+ * Static and remote resources coexist in the same org.
1110
1111
  */
1111
- private runtimeOrgs;
1112
+ private remoteResources;
1112
1113
  constructor(registry: OrganizationRegistry);
1113
1114
  /**
1114
1115
  * Validates registry on construction
@@ -1145,47 +1146,53 @@ declare class ResourceRegistry {
1145
1146
  */
1146
1147
  listAllResources(): OrganizationRegistry;
1147
1148
  /**
1148
- * Register an external organization at runtime
1149
+ * Register external resources at runtime
1149
1150
  *
1150
1151
  * Called during deploy pipeline when an external developer deploys their bundle.
1151
- * Adds the org's stub definitions to the registry and stores remote config
1152
- * for worker thread execution branching.
1152
+ * Merges the incoming stub definitions into the org's registry and stores
1153
+ * per-resource remote config for worker thread execution branching.
1153
1154
  *
1154
- * - If orgName conflicts with a static (startup) org, throws an error
1155
- * - If orgName is already runtime-registered (redeploy), unregisters first
1155
+ * Static and remote resources coexist in the same org. If the org already
1156
+ * has static resources, the incoming remote resources are merged alongside them.
1157
+ * If redeploying (some resources already registered as remote for this org),
1158
+ * the previous remote resources are unregistered first.
1156
1159
  *
1157
1160
  * @param orgName - Organization name (used as registry key)
1158
1161
  * @param org - Stub resource definitions (workflows/agents with placeholder handlers)
1159
1162
  * @param remote - Remote configuration (bundle path, deployment ID, env vars)
1160
- * @throws Error if orgName conflicts with a static organization
1163
+ * @throws Error if incoming resourceId conflicts with a static resource
1164
+ * @throws Error if incoming deployment contains duplicate resourceIds
1161
1165
  */
1162
1166
  registerOrganization(orgName: string, org: OrganizationResources, remote: RemoteOrgConfig): void;
1163
1167
  /**
1164
- * Unregister a runtime-registered organization
1168
+ * Unregister runtime-registered resources for an organization
1165
1169
  *
1166
- * Only removes organizations that were registered at runtime (via registerOrganization).
1167
- * Static organizations loaded at startup are protected and cannot be unregistered.
1168
- * No-op if the org is not runtime-registered.
1170
+ * Removes only resources that were registered at runtime (via registerOrganization).
1171
+ * Static resources loaded at startup are preserved. If the org still has static
1172
+ * resources after removal, the serialization cache is rebuilt. If no resources
1173
+ * remain, the org is fully removed from the registry.
1174
+ * No-op if the org has no remote resources.
1169
1175
  *
1170
- * @param orgName - Organization name to unregister
1176
+ * @param orgName - Organization name to unregister remote resources from
1171
1177
  */
1172
1178
  unregisterOrganization(orgName: string): void;
1173
1179
  /**
1174
- * Get remote configuration for an organization
1180
+ * Get remote configuration for a specific resource
1175
1181
  *
1176
- * Returns the RemoteOrgConfig if the org was registered at runtime,
1177
- * or null if it's a static org or doesn't exist.
1182
+ * Returns the RemoteOrgConfig if the resource was registered at runtime,
1183
+ * or null if it's a static resource or doesn't exist.
1178
1184
  * Used by the execution coordinator to branch between local and worker execution.
1179
1185
  *
1180
1186
  * @param orgName - Organization name
1187
+ * @param resourceId - Resource ID
1181
1188
  * @returns Remote config or null
1182
1189
  */
1183
- getRemoteConfig(orgName: string): RemoteOrgConfig | null;
1190
+ getRemoteConfig(orgName: string, resourceId: string): RemoteOrgConfig | null;
1184
1191
  /**
1185
- * Check if an organization is a remote (externally deployed) organization
1192
+ * Check if an organization has any remote (externally deployed) resources
1186
1193
  *
1187
1194
  * @param orgName - Organization name
1188
- * @returns true if the org was registered at runtime
1195
+ * @returns true if the org has at least one runtime-registered resource
1189
1196
  */
1190
1197
  isRemote(orgName: string): boolean;
1191
1198
  /**
@@ -1903,10 +1910,10 @@ declare class RegistryValidationError extends Error {
1903
1910
 
1904
1911
  /**
1905
1912
  * Project configuration for an external developer project.
1906
- * Defined in elevas.config.ts at the project root.
1913
+ * Defined in elevasis.config.ts at the project root.
1914
+ * Organization is derived from the ELEVASIS_API_KEY -- not configured here.
1907
1915
  */
1908
1916
  interface ElevasConfig {
1909
- organization: string;
1910
1917
  defaultStatus?: ResourceStatus;
1911
1918
  dev?: {
1912
1919
  port?: number;
package/dist/index.js CHANGED
@@ -3166,11 +3166,12 @@ var ResourceRegistry = class {
3166
3166
  */
3167
3167
  serializedCache;
3168
3168
  /**
3169
- * Runtime-registered organizations (external deployments)
3170
- * Tracks which orgs were added at runtime vs. static startup.
3171
- * Used to guard against unregistering static orgs and to store remote config.
3169
+ * Per-resource remote configuration (external deployments)
3170
+ * Key: "orgName/resourceId", Value: RemoteOrgConfig for that resource.
3171
+ * Tracks which individual resources were added at runtime via deploy pipeline.
3172
+ * Static and remote resources coexist in the same org.
3172
3173
  */
3173
- runtimeOrgs = /* @__PURE__ */ new Map();
3174
+ remoteResources = /* @__PURE__ */ new Map();
3174
3175
  /**
3175
3176
  * Validates registry on construction
3176
3177
  * - Checks for duplicate resourceIds within organizations
@@ -3273,67 +3274,125 @@ var ResourceRegistry = class {
3273
3274
  // Runtime Organization Registration (External Deployments)
3274
3275
  // ============================================================================
3275
3276
  /**
3276
- * Register an external organization at runtime
3277
+ * Register external resources at runtime
3277
3278
  *
3278
3279
  * Called during deploy pipeline when an external developer deploys their bundle.
3279
- * Adds the org's stub definitions to the registry and stores remote config
3280
- * for worker thread execution branching.
3280
+ * Merges the incoming stub definitions into the org's registry and stores
3281
+ * per-resource remote config for worker thread execution branching.
3281
3282
  *
3282
- * - If orgName conflicts with a static (startup) org, throws an error
3283
- * - If orgName is already runtime-registered (redeploy), unregisters first
3283
+ * Static and remote resources coexist in the same org. If the org already
3284
+ * has static resources, the incoming remote resources are merged alongside them.
3285
+ * If redeploying (some resources already registered as remote for this org),
3286
+ * the previous remote resources are unregistered first.
3284
3287
  *
3285
3288
  * @param orgName - Organization name (used as registry key)
3286
3289
  * @param org - Stub resource definitions (workflows/agents with placeholder handlers)
3287
3290
  * @param remote - Remote configuration (bundle path, deployment ID, env vars)
3288
- * @throws Error if orgName conflicts with a static organization
3291
+ * @throws Error if incoming resourceId conflicts with a static resource
3292
+ * @throws Error if incoming deployment contains duplicate resourceIds
3289
3293
  */
3290
3294
  registerOrganization(orgName, org, remote) {
3291
- if (this.registry[orgName] && !this.runtimeOrgs.has(orgName)) {
3292
- throw new Error(`Organization '${orgName}' conflicts with a static organization`);
3295
+ const incomingWorkflowIds = (org.workflows ?? []).map((w) => w.config.resourceId);
3296
+ const incomingAgentIds = (org.agents ?? []).map((a) => a.config.resourceId);
3297
+ const incomingIds = [...incomingWorkflowIds, ...incomingAgentIds];
3298
+ const seen = /* @__PURE__ */ new Set();
3299
+ for (const id of incomingIds) {
3300
+ if (seen.has(id)) {
3301
+ throw new Error(
3302
+ `Duplicate resource ID '${id}' in deployment. Each resource must have a unique ID.`
3303
+ );
3304
+ }
3305
+ seen.add(id);
3293
3306
  }
3294
- if (this.runtimeOrgs.has(orgName)) {
3307
+ if (this.isRemote(orgName)) {
3295
3308
  this.unregisterOrganization(orgName);
3296
3309
  }
3297
- this.registry[orgName] = org;
3298
- this.runtimeOrgs.set(orgName, remote);
3299
- this.serializedCache.set(orgName, serializeOrganization(org));
3310
+ const existingOrg = this.registry[orgName];
3311
+ if (existingOrg) {
3312
+ const staticWorkflowIds = new Set((existingOrg.workflows ?? []).map((w) => w.config.resourceId));
3313
+ const staticAgentIds = new Set((existingOrg.agents ?? []).map((a) => a.config.resourceId));
3314
+ for (const id of incomingIds) {
3315
+ if (staticWorkflowIds.has(id) || staticAgentIds.has(id)) {
3316
+ throw new Error(
3317
+ `Resource '${id}' already exists in '${orgName}' as an internal resource. External deployments cannot override internal resources.`
3318
+ );
3319
+ }
3320
+ }
3321
+ }
3322
+ if (existingOrg) {
3323
+ existingOrg.workflows = [...existingOrg.workflows ?? [], ...org.workflows ?? []];
3324
+ existingOrg.agents = [...existingOrg.agents ?? [], ...org.agents ?? []];
3325
+ } else {
3326
+ this.registry[orgName] = org;
3327
+ }
3328
+ for (const id of incomingIds) {
3329
+ this.remoteResources.set(`${orgName}/${id}`, remote);
3330
+ }
3331
+ this.serializedCache.set(orgName, serializeOrganization(this.registry[orgName]));
3300
3332
  }
3301
3333
  /**
3302
- * Unregister a runtime-registered organization
3334
+ * Unregister runtime-registered resources for an organization
3303
3335
  *
3304
- * Only removes organizations that were registered at runtime (via registerOrganization).
3305
- * Static organizations loaded at startup are protected and cannot be unregistered.
3306
- * No-op if the org is not runtime-registered.
3336
+ * Removes only resources that were registered at runtime (via registerOrganization).
3337
+ * Static resources loaded at startup are preserved. If the org still has static
3338
+ * resources after removal, the serialization cache is rebuilt. If no resources
3339
+ * remain, the org is fully removed from the registry.
3340
+ * No-op if the org has no remote resources.
3307
3341
  *
3308
- * @param orgName - Organization name to unregister
3342
+ * @param orgName - Organization name to unregister remote resources from
3309
3343
  */
3310
3344
  unregisterOrganization(orgName) {
3311
- if (!this.runtimeOrgs.has(orgName)) return;
3312
- delete this.registry[orgName];
3313
- this.runtimeOrgs.delete(orgName);
3314
- this.serializedCache.delete(orgName);
3345
+ const prefix = `${orgName}/`;
3346
+ const remoteIds = /* @__PURE__ */ new Set();
3347
+ for (const key of this.remoteResources.keys()) {
3348
+ if (key.startsWith(prefix)) {
3349
+ remoteIds.add(key.slice(prefix.length));
3350
+ this.remoteResources.delete(key);
3351
+ }
3352
+ }
3353
+ if (remoteIds.size === 0) return;
3354
+ const orgResources = this.registry[orgName];
3355
+ if (!orgResources) return;
3356
+ orgResources.workflows = (orgResources.workflows ?? []).filter(
3357
+ (w) => !remoteIds.has(w.config.resourceId)
3358
+ );
3359
+ orgResources.agents = (orgResources.agents ?? []).filter(
3360
+ (a) => !remoteIds.has(a.config.resourceId)
3361
+ );
3362
+ const remaining = (orgResources.workflows?.length ?? 0) + (orgResources.agents?.length ?? 0) + (orgResources.triggers?.length ?? 0) + (orgResources.integrations?.length ?? 0) + (orgResources.externalResources?.length ?? 0) + (orgResources.humanCheckpoints?.length ?? 0);
3363
+ if (remaining > 0) {
3364
+ this.serializedCache.set(orgName, serializeOrganization(orgResources));
3365
+ } else {
3366
+ delete this.registry[orgName];
3367
+ this.serializedCache.delete(orgName);
3368
+ }
3315
3369
  }
3316
3370
  /**
3317
- * Get remote configuration for an organization
3371
+ * Get remote configuration for a specific resource
3318
3372
  *
3319
- * Returns the RemoteOrgConfig if the org was registered at runtime,
3320
- * or null if it's a static org or doesn't exist.
3373
+ * Returns the RemoteOrgConfig if the resource was registered at runtime,
3374
+ * or null if it's a static resource or doesn't exist.
3321
3375
  * Used by the execution coordinator to branch between local and worker execution.
3322
3376
  *
3323
3377
  * @param orgName - Organization name
3378
+ * @param resourceId - Resource ID
3324
3379
  * @returns Remote config or null
3325
3380
  */
3326
- getRemoteConfig(orgName) {
3327
- return this.runtimeOrgs.get(orgName) ?? null;
3381
+ getRemoteConfig(orgName, resourceId) {
3382
+ return this.remoteResources.get(`${orgName}/${resourceId}`) ?? null;
3328
3383
  }
3329
3384
  /**
3330
- * Check if an organization is a remote (externally deployed) organization
3385
+ * Check if an organization has any remote (externally deployed) resources
3331
3386
  *
3332
3387
  * @param orgName - Organization name
3333
- * @returns true if the org was registered at runtime
3388
+ * @returns true if the org has at least one runtime-registered resource
3334
3389
  */
3335
3390
  isRemote(orgName) {
3336
- return this.runtimeOrgs.has(orgName);
3391
+ const prefix = `${orgName}/`;
3392
+ for (const key of this.remoteResources.keys()) {
3393
+ if (key.startsWith(prefix)) return true;
3394
+ }
3395
+ return false;
3337
3396
  }
3338
3397
  // ============================================================================
3339
3398
  // Resource Manifest Accessors
@@ -64,8 +64,12 @@ function startWorker(org) {
64
64
  const agents = new Map(
65
65
  (org.agents ?? []).map((a) => [a.config.resourceId, a])
66
66
  );
67
+ console.log(`[SDK-WORKER] Worker started with ${workflows.size} workflow(s), ${agents.size} agent(s)`);
67
68
  parentPort.on("message", async (msg) => {
68
69
  if (msg.type === "manifest") {
70
+ const workflowList = (org.workflows ?? []).map((w) => w.config.resourceId);
71
+ const agentList = (org.agents ?? []).map((a) => a.config.resourceId);
72
+ console.log(`[SDK-WORKER] Manifest requested: workflows=[${workflowList.join(", ")}], agents=[${agentList.join(", ")}]`);
69
73
  parentPort.postMessage({
70
74
  type: "manifest",
71
75
  workflows: (org.workflows ?? []).map((w) => ({
@@ -88,18 +92,24 @@ function startWorker(org) {
88
92
  return;
89
93
  }
90
94
  if (msg.type === "execute") {
91
- const { resourceId, input } = msg;
95
+ const { resourceId, executionId, input } = msg;
96
+ console.log(`[SDK-WORKER] Execute request: resourceId=${resourceId}, executionId=${executionId}`);
92
97
  const workflow = workflows.get(resourceId);
93
98
  if (workflow) {
94
99
  try {
100
+ console.log(`[SDK-WORKER] Running workflow '${resourceId}' (${Object.keys(workflow.steps).length} steps)`);
101
+ const startTime = Date.now();
95
102
  const { output, logs } = await executeWorkflow(workflow, input);
103
+ console.log(`[SDK-WORKER] Workflow '${resourceId}' completed (${Date.now() - startTime}ms)`);
96
104
  parentPort.postMessage({ type: "result", status: "completed", output, logs });
97
105
  } catch (err) {
106
+ console.error(`[SDK-WORKER] Workflow '${resourceId}' failed: ${String(err)}`);
98
107
  parentPort.postMessage({ type: "result", status: "failed", error: String(err), logs: [] });
99
108
  }
100
109
  return;
101
110
  }
102
111
  if (agents.has(resourceId)) {
112
+ console.error(`[SDK-WORKER] Agent execution not supported: ${resourceId}`);
103
113
  parentPort.postMessage({
104
114
  type: "result",
105
115
  status: "failed",
@@ -108,6 +118,7 @@ function startWorker(org) {
108
118
  });
109
119
  return;
110
120
  }
121
+ console.error(`[SDK-WORKER] Resource not found: ${resourceId}`);
111
122
  parentPort.postMessage({
112
123
  type: "result",
113
124
  status: "failed",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "SDK for building Elevasis organization resources",
5
5
  "comment:bin": "IMPORTANT: This package shares the 'elevasis' binary name with @repo/cli. They never conflict because @elevasis/sdk must NEVER be added as a dependency of any workspace package (apps/*, packages/*, organizations/*). Workspace projects use @repo/cli for the 'elevasis' binary. External developers (outside the workspace) get this SDK's binary via npm install.",
6
6
  "type": "module",