Skip to content

Commit a8ea743

Browse files
authored
fix: add workos seed --init to scaffold example seed file (#97)
* feat: add `workos seed --init` to scaffold example seed file Writes a commented workos-seed.yml with example permissions, roles, organizations, and config. Refuses to overwrite an existing file. Also updates the no-file error message to hint at --init. * chore: formatting
1 parent 38c19ec commit a8ea743

4 files changed

Lines changed: 126 additions & 4 deletions

File tree

src/bin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2011,13 +2011,14 @@ yargs(rawArgs)
20112011
'api-key': { type: 'string' as const, describe: 'WorkOS API key' },
20122012
file: { type: 'string', describe: 'Path to seed YAML file' },
20132013
clean: { type: 'boolean', default: false, describe: 'Tear down seeded resources' },
2014+
init: { type: 'boolean', default: false, describe: 'Create an example workos-seed.yml file' },
20142015
}),
20152016
async (argv) => {
20162017
await applyInsecureStorage(argv.insecureStorage);
20172018
const { resolveApiKey, resolveApiBaseUrl } = await import('./lib/api-key.js');
20182019
const { runSeed } = await import('./commands/seed.js');
20192020
await runSeed(
2020-
{ file: argv.file, clean: argv.clean },
2021+
{ file: argv.file, clean: argv.clean, init: argv.init },
20212022
resolveApiKey({ apiKey: argv.apiKey }),
20222023
resolveApiBaseUrl(),
20232024
);

src/commands/seed.spec.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ vi.mock('../lib/workos-client.js', () => ({
3232
}));
3333

3434
const { setOutputMode } = await import('../utils/output.js');
35-
const { runSeed } = await import('./seed.js');
35+
const { runSeed, runSeedInit } = await import('./seed.js');
3636

3737
const mockExistsSync = vi.mocked(existsSync);
3838
const mockReadFileSync = vi.mocked(readFileSync);
@@ -79,6 +79,55 @@ describe('seed command', () => {
7979

8080
afterEach(() => vi.restoreAllMocks());
8181

82+
describe('runSeedInit (--init)', () => {
83+
it('creates workos-seed.yml with example content', () => {
84+
mockExistsSync.mockReturnValue(false);
85+
86+
runSeedInit();
87+
88+
expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
89+
const [filePath, content] = mockWriteFileSync.mock.calls[0];
90+
expect(filePath).toBe('workos-seed.yml');
91+
expect(content).toContain('permissions:');
92+
expect(content).toContain('roles:');
93+
expect(content).toContain('organizations:');
94+
expect(content).toContain('config:');
95+
expect(content).toContain('redirect_uris:');
96+
expect(consoleOutput.some((l) => l.includes('Created'))).toBe(true);
97+
});
98+
99+
it('does not overwrite existing file', () => {
100+
mockExistsSync.mockReturnValue(true);
101+
102+
runSeedInit();
103+
104+
expect(mockWriteFileSync).not.toHaveBeenCalled();
105+
expect(consoleOutput.some((l) => l.includes('already exists'))).toBe(true);
106+
});
107+
108+
it('outputs JSON when in JSON mode', () => {
109+
setOutputMode('json');
110+
mockExistsSync.mockReturnValue(false);
111+
112+
runSeedInit();
113+
114+
const output = JSON.parse(consoleOutput[0]);
115+
expect(output.status).toBe('ok');
116+
expect(output.file).toBe('workos-seed.yml');
117+
setOutputMode('human');
118+
});
119+
120+
it('is reachable via runSeed({ init: true })', async () => {
121+
mockExistsSync.mockReturnValue(false);
122+
123+
await runSeed({ init: true }, 'sk_test');
124+
125+
expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
126+
const [filePath] = mockWriteFileSync.mock.calls[0];
127+
expect(filePath).toBe('workos-seed.yml');
128+
});
129+
});
130+
82131
describe('runSeed with --file', () => {
83132
it('creates resources in dependency order: permissions → roles → orgs → config', async () => {
84133
mockExistsSync.mockReturnValue(true);

src/commands/seed.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,74 @@ function saveState(state: SeedState): void {
3838
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
3939
}
4040

41+
const DEFAULT_SEED_FILE = 'workos-seed.yml';
42+
43+
const SEED_TEMPLATE = `# WorkOS seed file — provision resources with \`workos seed --file=${DEFAULT_SEED_FILE}\`
44+
# Resources are created in dependency order: permissions → roles → organizations → config.
45+
# Existing resources are skipped (idempotent). Run \`workos seed --clean\` to tear down.
46+
47+
permissions:
48+
- name: Read Posts
49+
slug: posts:read
50+
- name: Write Posts
51+
slug: posts:write
52+
description: Create and edit posts
53+
54+
roles:
55+
- name: Admin
56+
slug: admin
57+
description: Full access
58+
permissions:
59+
- posts:read
60+
- posts:write
61+
- name: Viewer
62+
slug: viewer
63+
permissions:
64+
- posts:read
65+
66+
organizations:
67+
- name: Acme Corp
68+
domains:
69+
- acme.com
70+
71+
config:
72+
redirect_uris:
73+
- http://localhost:3000/auth/callback
74+
cors_origins:
75+
- http://localhost:3000
76+
homepage_url: http://localhost:3000
77+
`;
78+
79+
export function runSeedInit(): void {
80+
if (existsSync(DEFAULT_SEED_FILE)) {
81+
if (isJsonMode()) {
82+
outputJson({ status: 'exists', message: `${DEFAULT_SEED_FILE} already exists`, file: DEFAULT_SEED_FILE });
83+
} else {
84+
console.log(chalk.yellow(`${DEFAULT_SEED_FILE} already exists — not overwriting.`));
85+
}
86+
return;
87+
}
88+
89+
writeFileSync(DEFAULT_SEED_FILE, SEED_TEMPLATE);
90+
91+
if (isJsonMode()) {
92+
outputJson({ status: 'ok', message: `Created ${DEFAULT_SEED_FILE}`, file: DEFAULT_SEED_FILE });
93+
} else {
94+
console.log(chalk.green(`Created ${DEFAULT_SEED_FILE}`));
95+
console.log(chalk.dim('Edit the file, then run: workos seed --file=workos-seed.yml'));
96+
}
97+
}
98+
4199
export async function runSeed(
42-
options: { file?: string; clean?: boolean },
100+
options: { file?: string; clean?: boolean; init?: boolean },
43101
apiKey: string,
44102
baseUrl?: string,
45103
): Promise<void> {
104+
if (options.init) {
105+
runSeedInit();
106+
return;
107+
}
108+
46109
if (options.clean) {
47110
await runSeedClean(apiKey, baseUrl);
48111
return;
@@ -51,7 +114,8 @@ export async function runSeed(
51114
if (!options.file) {
52115
return exitWithError({
53116
code: 'missing_args',
54-
message: 'Provide a seed file: workos seed --file=workos-seed.yml',
117+
message:
118+
'Provide a seed file: workos seed --file=workos-seed.yml\nRun workos seed --init to create an example seed file.',
55119
});
56120
}
57121

src/utils/help-json.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,14 @@ const commands: CommandSchema[] = [
10641064
default: false,
10651065
hidden: false,
10661066
},
1067+
{
1068+
name: 'init',
1069+
type: 'boolean',
1070+
description: 'Create an example workos-seed.yml file',
1071+
required: false,
1072+
default: false,
1073+
hidden: false,
1074+
},
10671075
],
10681076
},
10691077
{

0 commit comments

Comments
 (0)