allagents 0.2.2 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +32 -1
  2. package/dist/index.js +335 -45
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -130,9 +130,21 @@ allagents plugin validate <path>
130
130
 
131
131
  ## .allagents/workspace.yaml
132
132
 
133
- The workspace configuration file lives in `.allagents/workspace.yaml` and defines repositories, plugins, and target clients:
133
+ The workspace configuration file lives in `.allagents/workspace.yaml` and defines repositories, plugins, workspace files, and target clients:
134
134
 
135
135
  ```yaml
136
+ # Workspace file sync (optional) - copy files from a shared source
137
+ workspace:
138
+ source: ../shared-config # Default base for relative paths
139
+ files:
140
+ - AGENTS.md # String shorthand: same source and dest
141
+ - source: docs/guide.md # Object form: explicit source
142
+ dest: GUIDE.md # Optional dest (defaults to basename)
143
+ - dest: CUSTOM.md # File-level source override
144
+ source: ../other-config/CUSTOM.md
145
+ - dest: AGENTS.md # GitHub source
146
+ source: owner/repo/path/AGENTS.md
147
+
136
148
  repositories:
137
149
  - path: ../my-project
138
150
  owner: myorg
@@ -154,6 +166,25 @@ clients:
154
166
  - cursor
155
167
  ```
156
168
 
169
+ ### Workspace File Sync
170
+
171
+ The `workspace:` section enables syncing files from external sources to your workspace root. This is useful for sharing agent configurations (AGENTS.md, CLAUDE.md) across multiple projects.
172
+
173
+ **Key behaviors:**
174
+ - **Source of truth is remote** - Local copies are overwritten on every sync
175
+ - **Deleted files are restored** - If you delete AGENTS.md locally, sync restores it
176
+ - **WORKSPACE-RULES injection** - AGENTS.md and CLAUDE.md automatically get workspace discovery rules injected
177
+
178
+ **Source resolution:**
179
+ | Format | Example | Resolves to |
180
+ |--------|---------|-------------|
181
+ | String shorthand | `AGENTS.md` | `{workspace.source}/AGENTS.md` |
182
+ | Relative source | `source: docs/guide.md` | `{workspace.source}/docs/guide.md` |
183
+ | File-level override | `source: ../other/file.md` | `../other/file.md` (relative to workspace) |
184
+ | GitHub source | `source: owner/repo/path/file.md` | Fetched from GitHub cache |
185
+
186
+ **GitHub sources** are fetched fresh on every sync (always pulls latest).
187
+
157
188
  ### Plugin Spec Format
158
189
 
159
190
  Plugins use the `plugin@marketplace` format:
package/dist/index.js CHANGED
@@ -17720,12 +17720,12 @@ var RepositorySchema = exports_external.object({
17720
17720
  var WorkspaceFileSchema = exports_external.union([
17721
17721
  exports_external.string(),
17722
17722
  exports_external.object({
17723
- source: exports_external.string(),
17723
+ source: exports_external.string().optional(),
17724
17724
  dest: exports_external.string().optional()
17725
17725
  })
17726
17726
  ]);
17727
17727
  var WorkspaceSchema = exports_external.object({
17728
- source: exports_external.string(),
17728
+ source: exports_external.string().optional(),
17729
17729
  files: exports_external.array(WorkspaceFileSchema)
17730
17730
  });
17731
17731
  var PluginSourceSchema = exports_external.string();
@@ -19431,6 +19431,31 @@ function validatePluginSource(source) {
19431
19431
  }
19432
19432
  return { valid: true };
19433
19433
  }
19434
+ function parseFileSource(source, baseDir = process.cwd()) {
19435
+ if (isGitHubUrl(source)) {
19436
+ const parsed = parseGitHubUrl(source);
19437
+ if (parsed) {
19438
+ return {
19439
+ type: "github",
19440
+ original: source,
19441
+ normalized: source,
19442
+ owner: parsed.owner,
19443
+ repo: parsed.repo,
19444
+ ...parsed.subpath && { filePath: parsed.subpath }
19445
+ };
19446
+ }
19447
+ return {
19448
+ type: "local",
19449
+ original: source,
19450
+ normalized: normalizePluginPath(source, baseDir)
19451
+ };
19452
+ }
19453
+ return {
19454
+ type: "local",
19455
+ original: source,
19456
+ normalized: normalizePluginPath(source, baseDir)
19457
+ };
19458
+ }
19434
19459
  async function verifyGitHubUrlExists(source) {
19435
19460
  const parsed = parseGitHubUrl(source);
19436
19461
  if (!parsed) {
@@ -19943,8 +19968,56 @@ async function copyPluginToWorkspace(pluginPath, workspacePath, client, options2
19943
19968
  ]);
19944
19969
  return [...commandResults, ...skillResults, ...hookResults, ...agentResults];
19945
19970
  }
19971
+ function isExplicitGitHubSource(source) {
19972
+ if (source.startsWith("https://github.com/") || source.startsWith("http://github.com/") || source.startsWith("github.com/") || source.startsWith("gh:")) {
19973
+ return true;
19974
+ }
19975
+ if (!source.startsWith(".") && !source.startsWith("/") && source.includes("/")) {
19976
+ const parts = source.split("/");
19977
+ if (parts.length >= 3) {
19978
+ const validOwnerRepo = /^[a-zA-Z0-9_.-]+$/;
19979
+ if (parts[0] && parts[1] && validOwnerRepo.test(parts[0]) && validOwnerRepo.test(parts[1])) {
19980
+ return true;
19981
+ }
19982
+ }
19983
+ }
19984
+ return false;
19985
+ }
19986
+ function resolveFileSourcePath(source, defaultSourcePath, githubCache) {
19987
+ if (!isExplicitGitHubSource(source)) {
19988
+ if (source.startsWith("/")) {
19989
+ return { path: source };
19990
+ }
19991
+ if (source.startsWith("../")) {
19992
+ return { path: join4(process.cwd(), source) };
19993
+ }
19994
+ if (defaultSourcePath) {
19995
+ return { path: join4(defaultSourcePath, source) };
19996
+ }
19997
+ return { path: join4(process.cwd(), source) };
19998
+ }
19999
+ const parsed = parseFileSource(source);
20000
+ if (parsed.type === "github" && parsed.owner && parsed.repo && parsed.filePath) {
20001
+ const cacheKey = `${parsed.owner}/${parsed.repo}`;
20002
+ const cachePath = githubCache?.get(cacheKey);
20003
+ if (!cachePath) {
20004
+ return {
20005
+ path: "",
20006
+ error: `GitHub cache not found for ${cacheKey}. Ensure the repo is fetched.`
20007
+ };
20008
+ }
20009
+ return { path: join4(cachePath, parsed.filePath) };
20010
+ }
20011
+ if (parsed.type === "github") {
20012
+ return {
20013
+ path: "",
20014
+ error: `Invalid GitHub file source: ${source}. Must include path to file (e.g., owner/repo/path/to/file.md)`
20015
+ };
20016
+ }
20017
+ return null;
20018
+ }
19946
20019
  async function copyWorkspaceFiles(sourcePath, workspacePath, files, options2 = {}) {
19947
- const { dryRun = false } = options2;
20020
+ const { dryRun = false, githubCache } = options2;
19948
20021
  const results = [];
19949
20022
  const stringPatterns = [];
19950
20023
  const objectEntries = [];
@@ -19953,55 +20026,113 @@ async function copyWorkspaceFiles(sourcePath, workspacePath, files, options2 = {
19953
20026
  if (typeof file === "string") {
19954
20027
  stringPatterns.push(file);
19955
20028
  } else {
19956
- objectEntries.push({ source: file.source, dest: file.dest });
20029
+ let dest = file.dest;
20030
+ if (!dest && file.source) {
20031
+ const parts = file.source.split("/");
20032
+ dest = parts[parts.length - 1] || file.source;
20033
+ }
20034
+ if (!dest) {
20035
+ results.push({
20036
+ source: "unknown",
20037
+ destination: join4(workspacePath, "unknown"),
20038
+ action: "failed",
20039
+ error: "File entry must have at least source or dest specified"
20040
+ });
20041
+ continue;
20042
+ }
20043
+ objectEntries.push(file.source ? { source: file.source, dest } : { dest });
19957
20044
  }
19958
20045
  }
19959
20046
  if (stringPatterns.length > 0) {
19960
- const resolvedFiles = await resolveGlobPatterns(sourcePath, stringPatterns);
19961
- for (const resolved of resolvedFiles) {
19962
- const destPath = join4(workspacePath, resolved.relativePath);
19963
- if (!existsSync3(resolved.sourcePath)) {
19964
- const wasLiteral = stringPatterns.some((p) => !isGlobPattern(p) && !p.startsWith("!") && p === resolved.relativePath);
19965
- if (wasLiteral) {
20047
+ if (!sourcePath) {
20048
+ for (const pattern of stringPatterns) {
20049
+ if (!isGlobPattern(pattern) && !pattern.startsWith("!")) {
20050
+ results.push({
20051
+ source: pattern,
20052
+ destination: join4(workspacePath, pattern),
20053
+ action: "failed",
20054
+ error: `Cannot resolve file '${pattern}' - no workspace.source configured and no explicit source provided`
20055
+ });
20056
+ }
20057
+ }
20058
+ } else {
20059
+ const resolvedFiles = await resolveGlobPatterns(sourcePath, stringPatterns);
20060
+ for (const resolved of resolvedFiles) {
20061
+ const destPath = join4(workspacePath, resolved.relativePath);
20062
+ if (!existsSync3(resolved.sourcePath)) {
20063
+ const wasLiteral = stringPatterns.some((p) => !isGlobPattern(p) && !p.startsWith("!") && p === resolved.relativePath);
20064
+ if (wasLiteral) {
20065
+ results.push({
20066
+ source: resolved.sourcePath,
20067
+ destination: destPath,
20068
+ action: "failed",
20069
+ error: `Source file not found: ${resolved.sourcePath}`
20070
+ });
20071
+ }
20072
+ continue;
20073
+ }
20074
+ if (dryRun) {
20075
+ results.push({ source: resolved.sourcePath, destination: destPath, action: "copied" });
20076
+ if (AGENT_FILES2.includes(resolved.relativePath)) {
20077
+ copiedAgentFiles.push(resolved.relativePath);
20078
+ }
20079
+ continue;
20080
+ }
20081
+ try {
20082
+ await mkdir2(dirname2(destPath), { recursive: true });
20083
+ const content = await readFile3(resolved.sourcePath, "utf-8");
20084
+ await writeFile(destPath, content, "utf-8");
20085
+ results.push({ source: resolved.sourcePath, destination: destPath, action: "copied" });
20086
+ if (AGENT_FILES2.includes(resolved.relativePath)) {
20087
+ copiedAgentFiles.push(resolved.relativePath);
20088
+ }
20089
+ } catch (error) {
19966
20090
  results.push({
19967
20091
  source: resolved.sourcePath,
19968
20092
  destination: destPath,
19969
20093
  action: "failed",
19970
- error: `Source file not found: ${resolved.sourcePath}`
20094
+ error: error instanceof Error ? error.message : "Unknown error"
19971
20095
  });
19972
20096
  }
20097
+ }
20098
+ }
20099
+ }
20100
+ for (const entry of objectEntries) {
20101
+ const destPath = join4(workspacePath, entry.dest);
20102
+ let srcPath;
20103
+ if (entry.source) {
20104
+ const resolved = resolveFileSourcePath(entry.source, sourcePath, githubCache);
20105
+ if (!resolved) {
20106
+ results.push({
20107
+ source: entry.source,
20108
+ destination: destPath,
20109
+ action: "failed",
20110
+ error: `Failed to resolve source: ${entry.source}`
20111
+ });
19973
20112
  continue;
19974
20113
  }
19975
- if (dryRun) {
19976
- results.push({ source: resolved.sourcePath, destination: destPath, action: "copied" });
19977
- if (AGENT_FILES2.includes(resolved.relativePath)) {
19978
- copiedAgentFiles.push(resolved.relativePath);
19979
- }
20114
+ if (resolved.error) {
20115
+ results.push({
20116
+ source: entry.source,
20117
+ destination: destPath,
20118
+ action: "failed",
20119
+ error: resolved.error
20120
+ });
19980
20121
  continue;
19981
20122
  }
19982
- try {
19983
- await mkdir2(dirname2(destPath), { recursive: true });
19984
- const content = await readFile3(resolved.sourcePath, "utf-8");
19985
- await writeFile(destPath, content, "utf-8");
19986
- results.push({ source: resolved.sourcePath, destination: destPath, action: "copied" });
19987
- if (AGENT_FILES2.includes(resolved.relativePath)) {
19988
- copiedAgentFiles.push(resolved.relativePath);
19989
- }
19990
- } catch (error) {
20123
+ srcPath = resolved.path;
20124
+ } else {
20125
+ if (!sourcePath) {
19991
20126
  results.push({
19992
- source: resolved.sourcePath,
20127
+ source: entry.dest,
19993
20128
  destination: destPath,
19994
20129
  action: "failed",
19995
- error: error instanceof Error ? error.message : "Unknown error"
20130
+ error: `Cannot resolve file '${entry.dest}' - no workspace.source configured and no explicit source provided`
19996
20131
  });
20132
+ continue;
19997
20133
  }
20134
+ srcPath = join4(sourcePath, entry.dest);
19998
20135
  }
19999
- }
20000
- for (const entry of objectEntries) {
20001
- const srcPath = join4(sourcePath, entry.source);
20002
- const basename = entry.source.split("/").pop() || entry.source;
20003
- const destFilename = entry.dest ?? basename;
20004
- const destPath = join4(workspacePath, destFilename);
20005
20136
  if (!existsSync3(srcPath)) {
20006
20137
  results.push({
20007
20138
  source: srcPath,
@@ -20013,8 +20144,8 @@ async function copyWorkspaceFiles(sourcePath, workspacePath, files, options2 = {
20013
20144
  }
20014
20145
  if (dryRun) {
20015
20146
  results.push({ source: srcPath, destination: destPath, action: "copied" });
20016
- if (AGENT_FILES2.includes(destFilename)) {
20017
- copiedAgentFiles.push(destFilename);
20147
+ if (AGENT_FILES2.includes(entry.dest)) {
20148
+ copiedAgentFiles.push(entry.dest);
20018
20149
  }
20019
20150
  continue;
20020
20151
  }
@@ -20023,8 +20154,8 @@ async function copyWorkspaceFiles(sourcePath, workspacePath, files, options2 = {
20023
20154
  const content = await readFile3(srcPath, "utf-8");
20024
20155
  await writeFile(destPath, content, "utf-8");
20025
20156
  results.push({ source: srcPath, destination: destPath, action: "copied" });
20026
- if (AGENT_FILES2.includes(destFilename)) {
20027
- copiedAgentFiles.push(destFilename);
20157
+ if (AGENT_FILES2.includes(entry.dest)) {
20158
+ copiedAgentFiles.push(entry.dest);
20028
20159
  }
20029
20160
  } catch (error) {
20030
20161
  results.push({
@@ -20451,6 +20582,130 @@ async function cleanupEmptyParents(workspacePath, filePath) {
20451
20582
  }
20452
20583
  }
20453
20584
  }
20585
+ function isExplicitGitHubSourceForCollection(source) {
20586
+ if (source.startsWith("https://github.com/") || source.startsWith("http://github.com/") || source.startsWith("github.com/") || source.startsWith("gh:")) {
20587
+ return true;
20588
+ }
20589
+ if (!source.startsWith(".") && !source.startsWith("/") && source.includes("/")) {
20590
+ const parts = source.split("/");
20591
+ if (parts.length >= 3) {
20592
+ const validOwnerRepo = /^[a-zA-Z0-9_.-]+$/;
20593
+ if (parts[0] && parts[1] && validOwnerRepo.test(parts[0]) && validOwnerRepo.test(parts[1])) {
20594
+ return true;
20595
+ }
20596
+ }
20597
+ }
20598
+ return false;
20599
+ }
20600
+ function collectGitHubReposFromFiles(files) {
20601
+ const repos = new Map;
20602
+ for (const file of files) {
20603
+ if (typeof file === "string") {
20604
+ continue;
20605
+ }
20606
+ if (file.source && isExplicitGitHubSourceForCollection(file.source)) {
20607
+ const parsed = parseFileSource(file.source);
20608
+ if (parsed.type === "github" && parsed.owner && parsed.repo) {
20609
+ const key = `${parsed.owner}/${parsed.repo}`;
20610
+ if (!repos.has(key)) {
20611
+ repos.set(key, {
20612
+ owner: parsed.owner,
20613
+ repo: parsed.repo,
20614
+ key
20615
+ });
20616
+ }
20617
+ }
20618
+ }
20619
+ }
20620
+ return Array.from(repos.values());
20621
+ }
20622
+ async function fetchFileSourceRepos(repos, _force) {
20623
+ const cache = new Map;
20624
+ const errors2 = [];
20625
+ for (const repo of repos) {
20626
+ const result = await fetchPlugin(`${repo.owner}/${repo.repo}`, { force: true });
20627
+ if (result.success) {
20628
+ cache.set(repo.key, result.cachePath);
20629
+ } else {
20630
+ errors2.push(`Failed to fetch ${repo.key}: ${result.error || "Unknown error"}`);
20631
+ }
20632
+ }
20633
+ return { cache, errors: errors2 };
20634
+ }
20635
+ function isExplicitGitHubSourceForValidation(source) {
20636
+ if (source.startsWith("https://github.com/") || source.startsWith("http://github.com/") || source.startsWith("github.com/") || source.startsWith("gh:")) {
20637
+ return true;
20638
+ }
20639
+ if (!source.startsWith(".") && !source.startsWith("/") && source.includes("/")) {
20640
+ const parts = source.split("/");
20641
+ if (parts.length >= 3) {
20642
+ const validOwnerRepo = /^[a-zA-Z0-9_.-]+$/;
20643
+ if (parts[0] && parts[1] && validOwnerRepo.test(parts[0]) && validOwnerRepo.test(parts[1])) {
20644
+ return true;
20645
+ }
20646
+ }
20647
+ }
20648
+ return false;
20649
+ }
20650
+ function validateFileSources(files, defaultSourcePath, githubCache) {
20651
+ const errors2 = [];
20652
+ for (const file of files) {
20653
+ if (typeof file === "string") {
20654
+ if (!defaultSourcePath) {
20655
+ errors2.push(`Cannot resolve file '${file}' - no workspace.source configured`);
20656
+ continue;
20657
+ }
20658
+ const fullPath = join7(defaultSourcePath, file);
20659
+ if (!existsSync6(fullPath)) {
20660
+ errors2.push(`File source not found: ${fullPath}`);
20661
+ }
20662
+ continue;
20663
+ }
20664
+ if (file.source) {
20665
+ if (isExplicitGitHubSourceForValidation(file.source)) {
20666
+ const parsed = parseFileSource(file.source);
20667
+ if (!parsed.owner || !parsed.repo || !parsed.filePath) {
20668
+ errors2.push(`Invalid GitHub file source: ${file.source}. Must include path to file.`);
20669
+ continue;
20670
+ }
20671
+ const cacheKey = `${parsed.owner}/${parsed.repo}`;
20672
+ const cachePath = githubCache.get(cacheKey);
20673
+ if (!cachePath) {
20674
+ errors2.push(`GitHub cache not found for ${cacheKey}`);
20675
+ continue;
20676
+ }
20677
+ const fullPath = join7(cachePath, parsed.filePath);
20678
+ if (!existsSync6(fullPath)) {
20679
+ errors2.push(`Path not found in repository: ${cacheKey}/${parsed.filePath}`);
20680
+ }
20681
+ } else {
20682
+ let fullPath;
20683
+ if (file.source.startsWith("/")) {
20684
+ fullPath = file.source;
20685
+ } else if (file.source.startsWith("../")) {
20686
+ fullPath = resolve4(file.source);
20687
+ } else if (defaultSourcePath) {
20688
+ fullPath = join7(defaultSourcePath, file.source);
20689
+ } else {
20690
+ fullPath = resolve4(file.source);
20691
+ }
20692
+ if (!existsSync6(fullPath)) {
20693
+ errors2.push(`File source not found: ${fullPath}`);
20694
+ }
20695
+ }
20696
+ } else {
20697
+ if (!defaultSourcePath) {
20698
+ errors2.push(`Cannot resolve file '${file.dest}' - no workspace.source configured and no explicit source provided`);
20699
+ continue;
20700
+ }
20701
+ const fullPath = join7(defaultSourcePath, file.dest ?? "");
20702
+ if (!existsSync6(fullPath)) {
20703
+ errors2.push(`File source not found: ${fullPath}`);
20704
+ }
20705
+ }
20706
+ }
20707
+ return errors2;
20708
+ }
20454
20709
  function collectSyncedPaths(copyResults, workspacePath, clients) {
20455
20710
  const result = {};
20456
20711
  for (const client of clients) {
@@ -20623,17 +20878,52 @@ ${errors2}`
20623
20878
  }
20624
20879
  const pluginResults = await Promise.all(validatedPlugins.map((validatedPlugin) => copyValidatedPlugin(validatedPlugin, workspacePath, config.clients, dryRun)));
20625
20880
  let workspaceFileResults = [];
20626
- if (config.workspace && validatedWorkspaceSource) {
20627
- const sourcePath = validatedWorkspaceSource.resolved;
20881
+ if (config.workspace) {
20882
+ const sourcePath = validatedWorkspaceSource?.resolved;
20628
20883
  const filesToCopy = [...config.workspace.files];
20629
- for (const agentFile of AGENT_FILES) {
20630
- const agentPath = join7(sourcePath, agentFile);
20631
- if (existsSync6(agentPath) && !filesToCopy.includes(agentFile)) {
20632
- filesToCopy.push(agentFile);
20884
+ if (sourcePath) {
20885
+ for (const agentFile of AGENT_FILES) {
20886
+ const agentPath = join7(sourcePath, agentFile);
20887
+ if (existsSync6(agentPath) && !filesToCopy.includes(agentFile)) {
20888
+ filesToCopy.push(agentFile);
20889
+ }
20633
20890
  }
20634
20891
  }
20635
- workspaceFileResults = await copyWorkspaceFiles(sourcePath, workspacePath, filesToCopy, { dryRun });
20636
- if (!dryRun && config.clients.includes("claude")) {
20892
+ const fileSourceRepos = collectGitHubReposFromFiles(filesToCopy);
20893
+ let githubCache = new Map;
20894
+ if (fileSourceRepos.length > 0) {
20895
+ const { cache, errors: errors2 } = await fetchFileSourceRepos(fileSourceRepos, force);
20896
+ if (errors2.length > 0) {
20897
+ return {
20898
+ success: false,
20899
+ pluginResults,
20900
+ totalCopied: 0,
20901
+ totalFailed: errors2.length,
20902
+ totalSkipped: 0,
20903
+ totalGenerated: 0,
20904
+ error: `File source fetch failed (workspace unchanged):
20905
+ ${errors2.map((e) => ` - ${e}`).join(`
20906
+ `)}`
20907
+ };
20908
+ }
20909
+ githubCache = cache;
20910
+ }
20911
+ const fileValidationErrors = validateFileSources(filesToCopy, sourcePath, githubCache);
20912
+ if (fileValidationErrors.length > 0) {
20913
+ return {
20914
+ success: false,
20915
+ pluginResults,
20916
+ totalCopied: 0,
20917
+ totalFailed: fileValidationErrors.length,
20918
+ totalSkipped: 0,
20919
+ totalGenerated: 0,
20920
+ error: `File source validation failed (workspace unchanged):
20921
+ ${fileValidationErrors.map((e) => ` - ${e}`).join(`
20922
+ `)}`
20923
+ };
20924
+ }
20925
+ workspaceFileResults = await copyWorkspaceFiles(sourcePath, workspacePath, filesToCopy, { dryRun, githubCache });
20926
+ if (!dryRun && config.clients.includes("claude") && sourcePath) {
20637
20927
  const claudePath = join7(workspacePath, "CLAUDE.md");
20638
20928
  const agentsPath = join7(workspacePath, "AGENTS.md");
20639
20929
  const claudeExistsInSource = existsSync6(join7(sourcePath, "CLAUDE.md"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allagents",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tool for managing multi-repo AI agent workspaces with plugin synchronization",
5
5
  "type": "module",
6
6
  "bin": {