@alfe.ai/integrations 0.0.13 → 0.0.14

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/index.d.ts CHANGED
@@ -146,6 +146,7 @@ declare class InstallerError extends Error {
146
146
  }
147
147
  declare class Installer {
148
148
  private basePath;
149
+ private sharedPackagesReady;
149
150
  /**
150
151
  * @param basePath - Override the integrations directory (for testing).
151
152
  * Defaults to ~/.alfe/integrations/
@@ -159,6 +160,10 @@ declare class Installer {
159
160
  * Install an integration by cloning its git repo and checking out the pinned commit.
160
161
  * For monorepo integrations (with subdir), extracts only the subdir contents.
161
162
  *
163
+ * After cloning, ensures shared @alfe.ai packages are available at the root
164
+ * integrations directory, and runs `npm install --production` in the integration
165
+ * directory if it has its own package.json.
166
+ *
162
167
  * If the install path already exists (e.g. from a failed previous install),
163
168
  * it is cleaned up before retrying.
164
169
  */
@@ -180,6 +185,21 @@ declare class Installer {
180
185
  * Remove an installed integration.
181
186
  */
182
187
  remove(name: string): Promise<void>;
188
+ /**
189
+ * Ensure shared @alfe.ai packages are installed at the root integrations
190
+ * directory. Creates/updates a package.json and runs npm install.
191
+ *
192
+ * Node module resolution from hook scripts (e.g.
193
+ * ~/.alfe/integrations/console/hooks/post_install.js) walks up to
194
+ * ~/.alfe/integrations/node_modules/ and finds these packages.
195
+ */
196
+ private ensureSharedPackages;
197
+ /**
198
+ * Run `npm install --production` in an integration directory if it has
199
+ * its own package.json (for dependencies beyond the shared @alfe.ai packages).
200
+ */
201
+ private installLocalDependencies;
202
+ private runNpmInstall;
183
203
  /**
184
204
  * List all locally installed integrations.
185
205
  */
@@ -253,6 +273,7 @@ interface InstallTargets {
253
273
  interface IntegrationHooks {
254
274
  pre_install?: string;
255
275
  post_install?: string;
276
+ post_activate?: string;
256
277
  pre_uninstall?: string;
257
278
  post_uninstall?: string;
258
279
  health_check?: string;
@@ -537,8 +558,11 @@ declare class StateManager {
537
558
  /**
538
559
  * Hook Runner — executes integration lifecycle hook scripts.
539
560
  *
540
- * Hooks are shell scripts defined in the integration manifest.
541
- * They run as child processes with a 30-second timeout.
561
+ * Hooks are scripts defined in the integration manifest. The runner
562
+ * auto-detects the interpreter from the script's shebang line or file
563
+ * extension (.js/.mjs → node, .py → python3, default → bash).
564
+ *
565
+ * Scripts run as child processes with a 30-second timeout.
542
566
  * stdout/stderr are captured and returned.
543
567
  *
544
568
  * The runner injects standard environment variables:
@@ -575,9 +599,22 @@ interface HookEnvOptions {
575
599
  * - ALFE_<NAME_UPPER>_<KEY_UPPER>=value for each secret entry
576
600
  */
577
601
  declare function buildHookEnv(options: HookEnvOptions, additionalEnv?: Record<string, string>): Record<string, string>;
602
+ /**
603
+ * Resolve the interpreter for a hook script by inspecting its shebang
604
+ * line or file extension.
605
+ *
606
+ * Priority:
607
+ * 1. Shebang line (e.g. #!/usr/bin/env node → "node")
608
+ * 2. File extension (.js/.mjs → "node", .py → "python3")
609
+ * 3. Default → "bash"
610
+ */
611
+
578
612
  /**
579
613
  * Run a hook script from an integration directory.
580
614
  *
615
+ * The interpreter is auto-detected from the script's shebang line or
616
+ * file extension. See {@link resolveInterpreter}.
617
+ *
581
618
  * @param integrationPath - Base path of the integration (where alfe-integration.yaml lives)
582
619
  * @param hookScript - Relative path to the hook script (e.g. "scripts/activate.sh")
583
620
  * @param env - Additional environment variables to pass to the script
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { execFile, spawn } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { dirname, join } from "node:path";
4
4
  import { homedir, tmpdir } from "node:os";
5
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
+ import { closeSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { buildConfigValidationSchema, parseManifestFile } from "@alfe.ai/integration-manifest";
7
7
  import { createLogger } from "@auriclabs/logger";
8
8
  //#region src/registry.ts
@@ -124,10 +124,21 @@ var Resolver = class {
124
124
  * Each integration is cloned and checked out to a pinned commit hash.
125
125
  * For monorepo integrations (with subdir), only the subdir contents
126
126
  * are extracted — the full clone is discarded.
127
+ *
128
+ * Shared @alfe.ai packages (@alfe.ai/config, @alfe.ai/agent-api-client)
129
+ * are installed at the root integrations directory so all hook scripts
130
+ * can resolve them via Node's upward module resolution.
127
131
  */
128
132
  const execFileAsync$1 = promisify(execFile);
133
+ const log$1 = createLogger("Installer");
129
134
  const INTEGRATIONS_DIR = join(homedir(), ".alfe", "integrations");
130
135
  const GIT_TIMEOUT_MS = 6e4;
136
+ const NPM_TIMEOUT_MS = 6e4;
137
+ /** Shared @alfe.ai packages available to all integration hooks */
138
+ const SHARED_PACKAGES = {
139
+ "@alfe.ai/config": "latest",
140
+ "@alfe.ai/agent-api-client": "latest"
141
+ };
131
142
  var InstallerError = class extends Error {
132
143
  constructor(message) {
133
144
  super(message);
@@ -136,6 +147,7 @@ var InstallerError = class extends Error {
136
147
  };
137
148
  var Installer = class {
138
149
  basePath;
150
+ sharedPackagesReady = false;
139
151
  /**
140
152
  * @param basePath - Override the integrations directory (for testing).
141
153
  * Defaults to ~/.alfe/integrations/
@@ -153,6 +165,10 @@ var Installer = class {
153
165
  * Install an integration by cloning its git repo and checking out the pinned commit.
154
166
  * For monorepo integrations (with subdir), extracts only the subdir contents.
155
167
  *
168
+ * After cloning, ensures shared @alfe.ai packages are available at the root
169
+ * integrations directory, and runs `npm install --production` in the integration
170
+ * directory if it has its own package.json.
171
+ *
156
172
  * If the install path already exists (e.g. from a failed previous install),
157
173
  * it is cleaned up before retrying.
158
174
  */
@@ -165,6 +181,8 @@ var Installer = class {
165
181
  mkdirSync(this.basePath, { recursive: true });
166
182
  if (resolved.subdir) await this.cloneAndExtractSubdir(resolved, installPath);
167
183
  else await this.cloneDirect(resolved, installPath);
184
+ await this.ensureSharedPackages();
185
+ await this.installLocalDependencies(installPath);
168
186
  return installPath;
169
187
  }
170
188
  /**
@@ -251,6 +269,66 @@ var Installer = class {
251
269
  return Promise.resolve();
252
270
  }
253
271
  /**
272
+ * Ensure shared @alfe.ai packages are installed at the root integrations
273
+ * directory. Creates/updates a package.json and runs npm install.
274
+ *
275
+ * Node module resolution from hook scripts (e.g.
276
+ * ~/.alfe/integrations/console/hooks/post_install.js) walks up to
277
+ * ~/.alfe/integrations/node_modules/ and finds these packages.
278
+ */
279
+ async ensureSharedPackages() {
280
+ if (this.sharedPackagesReady) return;
281
+ const pkgJsonPath = join(this.basePath, "package.json");
282
+ const nodeModulesExists = existsSync(join(this.basePath, "node_modules"));
283
+ let needsInstall = false;
284
+ if (existsSync(pkgJsonPath)) try {
285
+ const deps = JSON.parse(readFileSync(pkgJsonPath, "utf-8")).dependencies ?? {};
286
+ needsInstall = Object.keys(SHARED_PACKAGES).some((pkg) => !(pkg in deps));
287
+ } catch {
288
+ needsInstall = true;
289
+ }
290
+ else needsInstall = true;
291
+ if (!needsInstall && !nodeModulesExists) needsInstall = true;
292
+ if (!needsInstall) {
293
+ this.sharedPackagesReady = true;
294
+ return;
295
+ }
296
+ const pkgJson = {
297
+ name: "alfe-integrations-root",
298
+ private: true,
299
+ type: "module",
300
+ dependencies: { ...SHARED_PACKAGES }
301
+ };
302
+ writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + "\n", "utf-8");
303
+ log$1.info("Installing shared @alfe.ai packages for integration hooks");
304
+ await this.runNpmInstall(this.basePath);
305
+ this.sharedPackagesReady = true;
306
+ }
307
+ /**
308
+ * Run `npm install --production` in an integration directory if it has
309
+ * its own package.json (for dependencies beyond the shared @alfe.ai packages).
310
+ */
311
+ async installLocalDependencies(installPath) {
312
+ if (!existsSync(join(installPath, "package.json"))) return;
313
+ log$1.info({ path: installPath }, "Installing integration-specific npm dependencies");
314
+ await this.runNpmInstall(installPath);
315
+ }
316
+ async runNpmInstall(cwd) {
317
+ try {
318
+ await execFileAsync$1("npm", [
319
+ "install",
320
+ "--production",
321
+ "--no-audit",
322
+ "--no-fund"
323
+ ], {
324
+ cwd,
325
+ timeout: NPM_TIMEOUT_MS
326
+ });
327
+ } catch (err) {
328
+ throw new InstallerError(`npm install failed in ${cwd}: ${err instanceof Error ? err.message : String(err)}`);
329
+ }
330
+ }
331
+ /**
254
332
  * List all locally installed integrations.
255
333
  */
256
334
  list() {
@@ -515,8 +593,11 @@ var LockManager = class {
515
593
  /**
516
594
  * Hook Runner — executes integration lifecycle hook scripts.
517
595
  *
518
- * Hooks are shell scripts defined in the integration manifest.
519
- * They run as child processes with a 30-second timeout.
596
+ * Hooks are scripts defined in the integration manifest. The runner
597
+ * auto-detects the interpreter from the script's shebang line or file
598
+ * extension (.js/.mjs → node, .py → python3, default → bash).
599
+ *
600
+ * Scripts run as child processes with a 30-second timeout.
520
601
  * stdout/stderr are captured and returned.
521
602
  *
522
603
  * The runner injects standard environment variables:
@@ -571,8 +652,55 @@ function buildHookEnv(options, additionalEnv) {
571
652
  return env;
572
653
  }
573
654
  /**
655
+ * Resolve the interpreter for a hook script by inspecting its shebang
656
+ * line or file extension.
657
+ *
658
+ * Priority:
659
+ * 1. Shebang line (e.g. #!/usr/bin/env node → "node")
660
+ * 2. File extension (.js/.mjs → "node", .py → "python3")
661
+ * 3. Default → "bash"
662
+ */
663
+ function resolveInterpreter(scriptPath) {
664
+ try {
665
+ const fd = openSync(scriptPath, "r");
666
+ const buf = Buffer.alloc(256);
667
+ readSync(fd, buf, 0, 256, 0);
668
+ closeSync(fd);
669
+ const firstLine = buf.toString("utf-8").split("\n")[0];
670
+ if (firstLine.startsWith("#!")) {
671
+ const shebang = firstLine.slice(2).trim();
672
+ if (shebang.startsWith("/usr/bin/env ")) {
673
+ const parts = shebang.slice(13).trim().split(/\s+/);
674
+ return {
675
+ command: parts[0],
676
+ args: [...parts.slice(1), scriptPath]
677
+ };
678
+ }
679
+ return {
680
+ command: shebang.split(/\s+/)[0],
681
+ args: [scriptPath]
682
+ };
683
+ }
684
+ } catch {}
685
+ if (scriptPath.endsWith(".js") || scriptPath.endsWith(".mjs")) return {
686
+ command: "node",
687
+ args: [scriptPath]
688
+ };
689
+ if (scriptPath.endsWith(".py")) return {
690
+ command: "python3",
691
+ args: [scriptPath]
692
+ };
693
+ return {
694
+ command: "bash",
695
+ args: [scriptPath]
696
+ };
697
+ }
698
+ /**
574
699
  * Run a hook script from an integration directory.
575
700
  *
701
+ * The interpreter is auto-detected from the script's shebang line or
702
+ * file extension. See {@link resolveInterpreter}.
703
+ *
576
704
  * @param integrationPath - Base path of the integration (where alfe-integration.yaml lives)
577
705
  * @param hookScript - Relative path to the hook script (e.g. "scripts/activate.sh")
578
706
  * @param env - Additional environment variables to pass to the script
@@ -586,11 +714,12 @@ async function runHook(integrationPath, hookScript, env) {
586
714
  stderr: `Hook script not found: ${scriptPath} (skipped)`,
587
715
  timedOut: false
588
716
  };
717
+ const { command, args } = resolveInterpreter(scriptPath);
589
718
  return new Promise((resolve) => {
590
719
  let stdout = "";
591
720
  let stderr = "";
592
721
  let timedOut = false;
593
- const proc = spawn("bash", [scriptPath], {
722
+ const proc = spawn(command, args, {
594
723
  cwd: integrationPath,
595
724
  env: {
596
725
  ...process.env,
@@ -757,7 +886,7 @@ var IntegrationManager = class {
757
886
  config: config ?? {},
758
887
  secrets: this.secrets.get(name)
759
888
  });
760
- if (hookResult.exitCode !== 0) this.log.warn(`post_install hook failed (non-fatal): ${hookResult.stderr || hookResult.stdout}`);
889
+ if (hookResult.exitCode !== 0) throw new Error(`post_install hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
761
890
  }
762
891
  this.state.set(name, {
763
892
  status: "installed",
@@ -894,6 +1023,18 @@ var IntegrationManager = class {
894
1023
  if (plugins.length > 0 || skills.length > 0) this.lockManager.addEntries(runtimeName, integrationId, manifest.version, plugins, skills, installPath);
895
1024
  }
896
1025
  this.state.setStatus(integrationId, "active");
1026
+ if (manifest.hooks.post_activate) {
1027
+ this.log.info(`Running post_activate hook: ${manifest.hooks.post_activate}`);
1028
+ const hookResult = await runHookWithContext(installPath, manifest.hooks.post_activate, {
1029
+ integrationName: integrationId,
1030
+ config: entry.config,
1031
+ secrets: this.secrets.get(integrationId)
1032
+ });
1033
+ if (hookResult.exitCode !== 0) {
1034
+ this.state.setStatus(integrationId, "error", `post_activate hook failed: ${hookResult.stderr || hookResult.stdout}`);
1035
+ return this.err("POST_ACTIVATE_FAILED", `post_activate hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1036
+ }
1037
+ }
897
1038
  if (manifest.hooks.health_check) {
898
1039
  this.log.info(`Running health check: ${manifest.hooks.health_check}`);
899
1040
  const hookResult = await runHookWithContext(installPath, manifest.hooks.health_check, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@auriclabs/logger": "^0.1.1",
16
- "@alfe.ai/integration-manifest": "^0.0.4"
16
+ "@alfe.ai/integration-manifest": "^0.0.5"
17
17
  },
18
18
  "files": [
19
19
  "dist"