@alfe.ai/integrations 0.0.3 → 0.0.5

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
@@ -8,11 +8,10 @@
8
8
  interface RegistryEntry {
9
9
  /** Human-readable display name (e.g. 'Alfe Voice') */
10
10
  name?: string;
11
- repo: string;
12
- /** GitHub repository URL (HTTPS, no .git suffix) for public clone */
13
- repository?: string;
14
- /** Pinned commit SHA — when present, install uses this exact commit instead of a tag */
15
- commit?: string;
11
+ /** GitHub repository URL (HTTPS) for public clone */
12
+ repository: string;
13
+ /** Git commit SHA — installer clones and checks out this exact commit */
14
+ commit: string;
16
15
  /** Subdirectory within the repo where the integration lives (for monorepos) */
17
16
  subdir?: string;
18
17
  versions: string[];
@@ -103,11 +102,10 @@ declare class Registry {
103
102
  interface ResolvedIntegration {
104
103
  id: string;
105
104
  name?: string;
106
- repo: string;
105
+ repository: string;
107
106
  version: string;
108
- tag: string;
109
- /** Pinned commit SHA — when present, installer uses this instead of tag */
110
- commit?: string;
107
+ /** Git commit SHA to checkout after clone */
108
+ commit: string;
111
109
  /** Subdirectory within the repo where the integration lives (for monorepos) */
112
110
  subdir?: string;
113
111
  description: string;
@@ -123,7 +121,7 @@ declare class Resolver {
123
121
  *
124
122
  * @param name - Integration name (e.g. "discord")
125
123
  * @param version - Specific version (e.g. "1.0.0") or undefined for latest
126
- * @returns Resolved integration with repo URL and git tag
124
+ * @returns Resolved integration with repo URL and commit hash
127
125
  */
128
126
  resolve(id: string, version?: string): Promise<ResolvedIntegration>;
129
127
  /**
@@ -148,8 +146,6 @@ declare class InstallerError extends Error {
148
146
  }
149
147
  declare class Installer {
150
148
  private basePath;
151
- /** Tracks subdir overrides per integration id */
152
- private subdirs;
153
149
  /**
154
150
  * @param basePath - Override the integrations directory (for testing).
155
151
  * Defaults to ~/.alfe/integrations/
@@ -157,20 +153,27 @@ declare class Installer {
157
153
  constructor(basePath?: string);
158
154
  /**
159
155
  * Get the path where an integration's content lives.
160
- * When a subdir is set, returns the subdir within the clone.
161
156
  */
162
157
  getInstallPath(name: string): string;
163
158
  /**
164
- * Get the root clone path (without subdir).
159
+ * Install an integration by cloning its git repo and checking out the pinned commit.
160
+ * For monorepo integrations (with subdir), extracts only the subdir contents.
161
+ *
162
+ * If the install path already exists (e.g. from a failed previous install),
163
+ * it is cleaned up before retrying.
165
164
  */
166
- getClonePath(name: string): string;
165
+ install(resolved: ResolvedIntegration): Promise<string>;
167
166
  /**
168
- * Install an integration by cloning its git repo and checking out the version tag.
169
- * Returns the effective install path (with subdir if present).
167
+ * Clone repo directly to the install path (no subdir).
170
168
  */
171
- install(resolved: ResolvedIntegration): Promise<string>;
169
+ private cloneDirect;
170
+ /**
171
+ * Clone monorepo to a temp dir, extract the subdir to the install path, discard the clone.
172
+ */
173
+ private cloneAndExtractSubdir;
172
174
  /**
173
175
  * Update an installed integration to a new version.
176
+ * Removes old install and does a fresh clone.
174
177
  */
175
178
  update(name: string, resolved: ResolvedIntegration): Promise<string>;
176
179
  /**
@@ -287,6 +290,8 @@ interface IntegrationManifest {
287
290
  config_schema: ConfigSchemaField[];
288
291
  capabilities: string[];
289
292
  hooks: IntegrationHooks;
293
+ /** Git repository URL (HTTPS, with .git suffix) — used by installer to clone */
294
+ repository: string;
290
295
  /**
291
296
  * Agent runtimes this integration supports.
292
297
  * If omitted or empty, the integration is considered universal (all runtimes).
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { dirname, join } from "node:path";
4
- import { homedir } from "node:os";
5
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, 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
@@ -83,7 +83,7 @@ var Resolver = class {
83
83
  *
84
84
  * @param name - Integration name (e.g. "discord")
85
85
  * @param version - Specific version (e.g. "1.0.0") or undefined for latest
86
- * @returns Resolved integration with repo URL and git tag
86
+ * @returns Resolved integration with repo URL and commit hash
87
87
  */
88
88
  async resolve(id, version) {
89
89
  const entry = await this.registry.get(id);
@@ -93,9 +93,8 @@ var Resolver = class {
93
93
  return {
94
94
  id,
95
95
  name: entry.name,
96
- repo: entry.repository ? `${entry.repository}.git` : entry.repo,
96
+ repository: entry.repository,
97
97
  version: resolvedVersion,
98
- tag: `v${resolvedVersion}`,
99
98
  commit: entry.commit,
100
99
  subdir: entry.subdir,
101
100
  description: entry.description
@@ -119,10 +118,12 @@ var Resolver = class {
119
118
  //#endregion
120
119
  //#region src/installer.ts
121
120
  /**
122
- * Installer — git clone/fetch integrations to ~/.alfe/integrations/{name}/.
121
+ * Installer — git clone integrations to ~/.alfe/integrations/{name}/.
123
122
  *
124
123
  * Manages the local installation directory for integration repos.
125
- * Each integration is a shallow git clone checked out to a version tag.
124
+ * Each integration is cloned and checked out to a pinned commit hash.
125
+ * For monorepo integrations (with subdir), only the subdir contents
126
+ * are extracted — the full clone is discarded.
126
127
  */
127
128
  const execFileAsync$1 = promisify(execFile);
128
129
  const INTEGRATIONS_DIR = join(homedir(), ".alfe", "integrations");
@@ -135,8 +136,6 @@ var InstallerError = class extends Error {
135
136
  };
136
137
  var Installer = class {
137
138
  basePath;
138
- /** Tracks subdir overrides per integration id */
139
- subdirs = /* @__PURE__ */ new Map();
140
139
  /**
141
140
  * @param basePath - Override the integrations directory (for testing).
142
141
  * Defaults to ~/.alfe/integrations/
@@ -146,100 +145,109 @@ var Installer = class {
146
145
  }
147
146
  /**
148
147
  * Get the path where an integration's content lives.
149
- * When a subdir is set, returns the subdir within the clone.
150
148
  */
151
149
  getInstallPath(name) {
152
- const base = join(this.basePath, name);
153
- const subdir = this.subdirs.get(name);
154
- return subdir ? join(base, subdir) : base;
155
- }
156
- /**
157
- * Get the root clone path (without subdir).
158
- */
159
- getClonePath(name) {
160
150
  return join(this.basePath, name);
161
151
  }
162
152
  /**
163
- * Install an integration by cloning its git repo and checking out the version tag.
164
- * Returns the effective install path (with subdir if present).
153
+ * Install an integration by cloning its git repo and checking out the pinned commit.
154
+ * For monorepo integrations (with subdir), extracts only the subdir contents.
155
+ *
156
+ * If the install path already exists (e.g. from a failed previous install),
157
+ * it is cleaned up before retrying.
165
158
  */
166
159
  async install(resolved) {
167
- const clonePath = this.getClonePath(resolved.id);
168
- if (existsSync(clonePath)) throw new InstallerError(`Integration "${resolved.id}" is already installed at ${clonePath}`);
169
- if (resolved.subdir) this.subdirs.set(resolved.id, resolved.subdir);
160
+ const installPath = this.getInstallPath(resolved.id);
161
+ if (existsSync(installPath)) rmSync(installPath, {
162
+ recursive: true,
163
+ force: true
164
+ });
170
165
  mkdirSync(this.basePath, { recursive: true });
166
+ if (resolved.subdir) await this.cloneAndExtractSubdir(resolved, installPath);
167
+ else await this.cloneDirect(resolved, installPath);
168
+ return installPath;
169
+ }
170
+ /**
171
+ * Clone repo directly to the install path (no subdir).
172
+ */
173
+ async cloneDirect(resolved, installPath) {
171
174
  try {
172
- if (resolved.commit) {
173
- await execFileAsync$1("git", [
174
- "clone",
175
- resolved.repo,
176
- clonePath
177
- ], { timeout: GIT_TIMEOUT_MS });
178
- await execFileAsync$1("git", ["checkout", resolved.commit], {
179
- cwd: clonePath,
180
- timeout: GIT_TIMEOUT_MS
181
- });
182
- } else await execFileAsync$1("git", [
175
+ await execFileAsync$1("git", [
183
176
  "clone",
184
- "--depth",
185
- "1",
186
- "--branch",
187
- resolved.tag,
188
- resolved.repo,
189
- clonePath
177
+ resolved.repository,
178
+ installPath
190
179
  ], { timeout: GIT_TIMEOUT_MS });
180
+ await execFileAsync$1("git", ["checkout", resolved.commit], {
181
+ cwd: installPath,
182
+ timeout: GIT_TIMEOUT_MS
183
+ });
191
184
  } catch (err) {
192
- if (existsSync(clonePath)) rmSync(clonePath, {
185
+ if (existsSync(installPath)) rmSync(installPath, {
193
186
  recursive: true,
194
187
  force: true
195
188
  });
196
- this.subdirs.delete(resolved.id);
197
189
  const message = err instanceof Error ? err.message : String(err);
198
190
  throw new InstallerError(`Failed to clone integration "${resolved.id}": ${message}`);
199
191
  }
200
- return this.getInstallPath(resolved.id);
201
192
  }
202
193
  /**
203
- * Update an installed integration to a new version.
194
+ * Clone monorepo to a temp dir, extract the subdir to the install path, discard the clone.
204
195
  */
205
- async update(name, resolved) {
206
- const clonePath = this.getClonePath(name);
207
- if (!existsSync(clonePath)) throw new InstallerError(`Integration "${name}" is not installed — cannot update`);
208
- if (resolved.subdir) this.subdirs.set(name, resolved.subdir);
209
- else this.subdirs.delete(name);
196
+ async cloneAndExtractSubdir(resolved, installPath) {
197
+ if (!resolved.subdir) throw new InstallerError("subdir is required for monorepo extraction");
198
+ const subdir = resolved.subdir;
199
+ const tempDir = mkdtempSync(join(tmpdir(), `alfe-clone-${resolved.id}-`));
210
200
  try {
211
201
  await execFileAsync$1("git", [
212
- "fetch",
213
- "--depth",
214
- "1",
215
- "origin",
216
- `tag`,
217
- resolved.tag
218
- ], {
219
- cwd: clonePath,
220
- timeout: GIT_TIMEOUT_MS
221
- });
222
- await execFileAsync$1("git", ["checkout", resolved.tag], {
223
- cwd: clonePath,
202
+ "clone",
203
+ resolved.repository,
204
+ tempDir
205
+ ], { timeout: GIT_TIMEOUT_MS });
206
+ await execFileAsync$1("git", ["checkout", resolved.commit], {
207
+ cwd: tempDir,
224
208
  timeout: GIT_TIMEOUT_MS
225
209
  });
210
+ const subdirPath = join(tempDir, subdir);
211
+ if (!existsSync(subdirPath)) throw new InstallerError(`Subdir "${subdir}" not found in repo after checkout`);
212
+ cpSync(subdirPath, installPath, { recursive: true });
226
213
  } catch (err) {
214
+ if (existsSync(installPath)) rmSync(installPath, {
215
+ recursive: true,
216
+ force: true
217
+ });
218
+ if (err instanceof InstallerError) throw err;
227
219
  const message = err instanceof Error ? err.message : String(err);
228
- throw new InstallerError(`Failed to update integration "${name}" to ${resolved.version}: ${message}`);
220
+ throw new InstallerError(`Failed to clone integration "${resolved.id}": ${message}`);
221
+ } finally {
222
+ rmSync(tempDir, {
223
+ recursive: true,
224
+ force: true
225
+ });
229
226
  }
230
- return this.getInstallPath(name);
227
+ }
228
+ /**
229
+ * Update an installed integration to a new version.
230
+ * Removes old install and does a fresh clone.
231
+ */
232
+ async update(name, resolved) {
233
+ const installPath = this.getInstallPath(name);
234
+ if (!existsSync(installPath)) throw new InstallerError(`Integration "${name}" is not installed — cannot update`);
235
+ rmSync(installPath, {
236
+ recursive: true,
237
+ force: true
238
+ });
239
+ return this.install(resolved);
231
240
  }
232
241
  /**
233
242
  * Remove an installed integration.
234
243
  */
235
244
  remove(name) {
236
- const clonePath = this.getClonePath(name);
237
- if (!existsSync(clonePath)) throw new InstallerError(`Integration "${name}" is not installed at ${clonePath}`);
238
- rmSync(clonePath, {
245
+ const installPath = this.getInstallPath(name);
246
+ if (!existsSync(installPath)) throw new InstallerError(`Integration "${name}" is not installed at ${installPath}`);
247
+ rmSync(installPath, {
239
248
  recursive: true,
240
249
  force: true
241
250
  });
242
- this.subdirs.delete(name);
243
251
  return Promise.resolve();
244
252
  }
245
253
  /**
@@ -276,7 +284,7 @@ var Installer = class {
276
284
  * Check if an integration is installed.
277
285
  */
278
286
  isInstalled(name) {
279
- return existsSync(this.getClonePath(name));
287
+ return existsSync(this.getInstallPath(name));
280
288
  }
281
289
  };
282
290
  //#endregion
@@ -711,7 +719,7 @@ var IntegrationManager = class {
711
719
  });
712
720
  try {
713
721
  const resolved = await this.resolver.resolve(name, version);
714
- this.log.info(`Resolved ${name}@${resolved.version} from ${resolved.repo}`);
722
+ this.log.info(`Resolved ${name}@${resolved.version} from ${resolved.repository}`);
715
723
  const installPath = await this.installer.install(resolved);
716
724
  this.log.info(`Cloned to ${installPath}`);
717
725
  const manifestPath = join(installPath, "alfe-integration.yaml");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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.2"
16
+ "@alfe.ai/integration-manifest": "^0.0.3"
17
17
  },
18
18
  "files": [
19
19
  "dist"