Skip to content

Commit 6d968ea

Browse files
feat(init): generate AGENTS.md with plugin development conventions (#109)
1 parent 1f2e19d commit 6d968ea

5 files changed

Lines changed: 229 additions & 0 deletions

File tree

cli/.justfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ setup:
88
build: setup
99
npx tsc
1010
cp src/viewer/index.html src/viewer/viewer.css src/viewer/empty-plugin-styles.css dist/viewer/
11+
cp src/agents.template.md dist/
1112
npx oclif manifest
1213

1314
# Run static checks

cli/src/agents.template.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Workday Everywhere Plugin — Agent Guide
2+
3+
This project is a **Workday Everywhere (WE) plugin** built with the `@workday/everywhere` SDK.
4+
5+
## Example applications
6+
7+
Reference implementations are available in the SDK repository:
8+
9+
- **hello** — minimal single-page plugin:
10+
https://github.com/Workday/everywhere/tree/main/examples/hello
11+
- **directory** — multi-page plugin with routes and data access:
12+
https://github.com/Workday/everywhere/tree/main/examples/directory
13+
14+
## Entry point
15+
16+
`plugin.tsx` must default-export a `PluginDefinition` created with `plugin()`:
17+
18+
```typescript
19+
import { plugin, route } from '@workday/everywhere';
20+
export default plugin({ defaultRoute, routes, provider });
21+
```
22+
23+
## Routes and navigation
24+
25+
Define routes with `route()`. Add a type parameter to enable type-safe params:
26+
27+
```typescript
28+
const list = route('list', { component: ListPage });
29+
const detail = route<{ id: string }>('detail', { component: DetailPage });
30+
```
31+
32+
Navigate between routes with `useNavigate()` and read params with `useParams()`:
33+
34+
```typescript
35+
import { useNavigate, useParams } from '@workday/everywhere';
36+
const navigate = useNavigate();
37+
navigate(detail, { id: '123' });
38+
const { id } = useParams(detail);
39+
```
40+
41+
## Data layer
42+
43+
This plugin runs inside a **data provider** context. Data access goes through a `DataResolver`
44+
wrapped in `DataProvider`. Pass the provider to `plugin()` so every page can access it:
45+
46+
```typescript
47+
import { DataProvider, GraphQLResolver } from '@workday/everywhere';
48+
import { schema } from './everywhere/data/schema.js';
49+
50+
const resolver = new GraphQLResolver('your-app-referenceId', schema);
51+
52+
function Provider({ children }: { children: React.ReactNode }) {
53+
return <DataProvider resolver={resolver}>{children}</DataProvider>;
54+
}
55+
56+
export default plugin({ defaultRoute, routes, provider: Provider });
57+
```
58+
59+
Data hooks are auto-generated by `npx everywhere bind` into `everywhere/data/` and follow this
60+
pattern:
61+
62+
```typescript
63+
import { useEmployees } from './everywhere/data/Employee.js';
64+
const { data, loading, error } = useEmployees();
65+
```
66+
67+
## Peer dependencies
68+
69+
`react` and `react-dom` (>=18) are **peer dependencies** — they are provided by the host environment
70+
and must not be bundled with the plugin.
71+
72+
## Import conventions
73+
74+
All local imports must use `.js` extensions, even in `.ts`/`.tsx` source files:
75+
76+
```typescript
77+
import { helper } from './utils.js'; // correct
78+
import { helper } from './utils'; // incorrect — omitting .js breaks ESM resolution
79+
```
80+
81+
## CLI reference
82+
83+
- `npx everywhere dev` — start the dev server with hot reload
84+
- `npx everywhere build` — build the plugin bundle
85+
- `npx everywhere bind <bundle>` — generate typed data hooks from a business object bundle

cli/src/commands/everywhere/init.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,17 @@ export function writeTsConfigIfAbsent(pluginDir: string): boolean {
104104
return true;
105105
}
106106

107+
const AGENTS_TEMPLATE_PATH = path.resolve(THIS_DIR, '../../agents.template.md');
108+
109+
export function writeAgentsMdIfAbsent(pluginDir: string): boolean {
110+
const agentsPath = path.join(pluginDir, 'AGENTS.md');
111+
if (fs.existsSync(agentsPath)) {
112+
return false;
113+
}
114+
fs.copyFileSync(AGENTS_TEMPLATE_PATH, agentsPath);
115+
return true;
116+
}
117+
107118
export function runNpmInstall(cwd: string): Promise<void> {
108119
return new Promise((resolve, reject) => {
109120
const child = spawn(NPM_BIN, ['install'], {
@@ -268,6 +279,14 @@ export default class InitCommand extends EverywhereBaseCommand {
268279
this.log(`tsconfig.json already exists, skipping (${chalk.cyan(tsConfigPath)})`);
269280
}
270281

282+
// Mutation 4: write AGENTS.md if not already present
283+
const agentsPath = path.join(pluginDir, 'AGENTS.md');
284+
if (writeAgentsMdIfAbsent(pluginDir)) {
285+
this.log(chalk.green('Created AGENTS.md'));
286+
} else if (verbose) {
287+
this.log(`AGENTS.md already exists, skipping (${chalk.cyan(agentsPath)})`);
288+
}
289+
271290
// Run npm install
272291
this.log('Installing dependencies...');
273292
await runNpmInstall(pluginDir);

cli/tests/commands/everywhere/init.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as fs from 'node:fs';
55
import InitCommand, {
66
resolveTypeDevDependencies,
77
writeTsConfigIfAbsent,
8+
writeAgentsMdIfAbsent,
89
} from '../../../src/commands/everywhere/init.js';
910
import EverywhereBaseCommand from '../../../src/lib/command.js';
1011

@@ -330,6 +331,51 @@ describe('writeTsConfigIfAbsent', () => {
330331
});
331332
});
332333

334+
describe('writeAgentsMdIfAbsent', () => {
335+
let tmpDir: string;
336+
337+
beforeEach(() => {
338+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'we-init-test-'));
339+
});
340+
341+
afterEach(() => {
342+
fs.rmSync(tmpDir, { recursive: true });
343+
});
344+
345+
describe('when AGENTS.md does not exist', () => {
346+
it('writes AGENTS.md to the directory', () => {
347+
writeAgentsMdIfAbsent(tmpDir);
348+
expect(fs.existsSync(path.join(tmpDir, 'AGENTS.md'))).toBe(true);
349+
});
350+
351+
it('writes non-empty content', () => {
352+
writeAgentsMdIfAbsent(tmpDir);
353+
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
354+
expect(content.length).toBeGreaterThan(0);
355+
});
356+
357+
it('returns true', () => {
358+
expect(writeAgentsMdIfAbsent(tmpDir)).toBe(true);
359+
});
360+
});
361+
362+
describe('when AGENTS.md already exists', () => {
363+
beforeEach(() => {
364+
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# existing\n');
365+
});
366+
367+
it('does not overwrite the existing file', () => {
368+
writeAgentsMdIfAbsent(tmpDir);
369+
const content = fs.readFileSync(path.join(tmpDir, 'AGENTS.md'), 'utf-8');
370+
expect(content).toBe('# existing\n');
371+
});
372+
373+
it('returns false', () => {
374+
expect(writeAgentsMdIfAbsent(tmpDir)).toBe(false);
375+
});
376+
});
377+
});
378+
333379
describe('runNpmInit', () => {
334380
beforeEach(() => {
335381
vi.resetModules();

cli/tests/init-template.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { describe, it, expect } from 'vitest';
2+
import * as fs from 'node:fs';
3+
import * as path from 'node:path';
4+
import { fileURLToPath } from 'node:url';
25
import { renderStub, renderTsConfig } from '../src/init-template.js';
36

7+
const TEMPLATE_PATH = path.resolve(
8+
path.dirname(fileURLToPath(import.meta.url)),
9+
'../src/agents.template.md'
10+
);
11+
412
describe('renderStub', () => {
513
describe('when called with a name', () => {
614
it('returns a string', () => {
@@ -104,3 +112,73 @@ describe('renderTsConfig', () => {
104112
});
105113
});
106114
});
115+
116+
describe('agents.template.md', () => {
117+
let content: string;
118+
119+
describe('when read from disk', () => {
120+
it('exists', () => {
121+
expect(fs.existsSync(TEMPLATE_PATH)).toBe(true);
122+
});
123+
124+
it('ends with a newline', () => {
125+
content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
126+
expect(content.endsWith('\n')).toBe(true);
127+
});
128+
});
129+
130+
describe('project context', () => {
131+
it('identifies this as a Workday Everywhere plugin project', () => {
132+
content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
133+
expect(content).toContain('Workday Everywhere');
134+
});
135+
136+
it('names the entry point file', () => {
137+
content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
138+
expect(content).toContain('plugin.tsx');
139+
});
140+
});
141+
142+
describe('data provider guidance', () => {
143+
it('mentions DataProvider', () => {
144+
content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
145+
expect(content).toContain('DataProvider');
146+
});
147+
148+
it('mentions DataResolver', () => {
149+
content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
150+
expect(content).toContain('DataResolver');
151+
});
152+
});
153+
154+
describe('peer dependency guidance', () => {
155+
it('mentions react as a peer dependency', () => {
156+
content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
157+
expect(content.toLowerCase()).toContain('peer');
158+
});
159+
160+
it('mentions react', () => {
161+
content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
162+
expect(content).toContain('react');
163+
});
164+
});
165+
166+
describe('import convention guidance', () => {
167+
it('calls out the .js extension requirement', () => {
168+
content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
169+
expect(content).toContain('.js');
170+
});
171+
});
172+
173+
describe('example references', () => {
174+
it('links to the hello example', () => {
175+
content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
176+
expect(content).toContain('examples/hello');
177+
});
178+
179+
it('links to the directory example', () => {
180+
content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
181+
expect(content).toContain('examples/directory');
182+
});
183+
});
184+
});

0 commit comments

Comments
 (0)