Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2011,13 +2011,14 @@ yargs(rawArgs)
'api-key': { type: 'string' as const, describe: 'WorkOS API key' },
file: { type: 'string', describe: 'Path to seed YAML file' },
clean: { type: 'boolean', default: false, describe: 'Tear down seeded resources' },
init: { type: 'boolean', default: false, describe: 'Create an example workos-seed.yml file' },
}),
async (argv) => {
await applyInsecureStorage(argv.insecureStorage);
const { resolveApiKey, resolveApiBaseUrl } = await import('./lib/api-key.js');
const { runSeed } = await import('./commands/seed.js');
await runSeed(
{ file: argv.file, clean: argv.clean },
{ file: argv.file, clean: argv.clean, init: argv.init },
resolveApiKey({ apiKey: argv.apiKey }),
resolveApiBaseUrl(),
);
Expand Down
51 changes: 50 additions & 1 deletion src/commands/seed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ vi.mock('../lib/workos-client.js', () => ({
}));

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

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

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

describe('runSeedInit (--init)', () => {
it('creates workos-seed.yml with example content', () => {
mockExistsSync.mockReturnValue(false);

runSeedInit();

expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
const [filePath, content] = mockWriteFileSync.mock.calls[0];
expect(filePath).toBe('workos-seed.yml');
expect(content).toContain('permissions:');
expect(content).toContain('roles:');
expect(content).toContain('organizations:');
expect(content).toContain('config:');
expect(content).toContain('redirect_uris:');
expect(consoleOutput.some((l) => l.includes('Created'))).toBe(true);
});

it('does not overwrite existing file', () => {
mockExistsSync.mockReturnValue(true);

runSeedInit();

expect(mockWriteFileSync).not.toHaveBeenCalled();
expect(consoleOutput.some((l) => l.includes('already exists'))).toBe(true);
});

it('outputs JSON when in JSON mode', () => {
setOutputMode('json');
mockExistsSync.mockReturnValue(false);

runSeedInit();

const output = JSON.parse(consoleOutput[0]);
expect(output.status).toBe('ok');
expect(output.file).toBe('workos-seed.yml');
setOutputMode('human');
});

it('is reachable via runSeed({ init: true })', async () => {
mockExistsSync.mockReturnValue(false);

await runSeed({ init: true }, 'sk_test');

expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
const [filePath] = mockWriteFileSync.mock.calls[0];
expect(filePath).toBe('workos-seed.yml');
});
});

describe('runSeed with --file', () => {
it('creates resources in dependency order: permissions → roles → orgs → config', async () => {
mockExistsSync.mockReturnValue(true);
Expand Down
68 changes: 66 additions & 2 deletions src/commands/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,74 @@ function saveState(state: SeedState): void {
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}

const DEFAULT_SEED_FILE = 'workos-seed.yml';

const SEED_TEMPLATE = `# WorkOS seed file — provision resources with \`workos seed --file=${DEFAULT_SEED_FILE}\`
# Resources are created in dependency order: permissions → roles → organizations → config.
# Existing resources are skipped (idempotent). Run \`workos seed --clean\` to tear down.

permissions:
- name: Read Posts
slug: posts:read
- name: Write Posts
slug: posts:write
description: Create and edit posts

roles:
- name: Admin
slug: admin
description: Full access
permissions:
- posts:read
- posts:write
- name: Viewer
slug: viewer
permissions:
- posts:read

organizations:
- name: Acme Corp
domains:
- acme.com

config:
redirect_uris:
- http://localhost:3000/auth/callback
cors_origins:
- http://localhost:3000
homepage_url: http://localhost:3000
`;

export function runSeedInit(): void {
if (existsSync(DEFAULT_SEED_FILE)) {
if (isJsonMode()) {
outputJson({ status: 'exists', message: `${DEFAULT_SEED_FILE} already exists`, file: DEFAULT_SEED_FILE });
} else {
console.log(chalk.yellow(`${DEFAULT_SEED_FILE} already exists — not overwriting.`));
}
return;
}

writeFileSync(DEFAULT_SEED_FILE, SEED_TEMPLATE);

if (isJsonMode()) {
outputJson({ status: 'ok', message: `Created ${DEFAULT_SEED_FILE}`, file: DEFAULT_SEED_FILE });
} else {
console.log(chalk.green(`Created ${DEFAULT_SEED_FILE}`));
console.log(chalk.dim('Edit the file, then run: workos seed --file=workos-seed.yml'));
}
}

export async function runSeed(
options: { file?: string; clean?: boolean },
options: { file?: string; clean?: boolean; init?: boolean },
apiKey: string,
baseUrl?: string,
): Promise<void> {
if (options.init) {
runSeedInit();
return;
}

if (options.clean) {
await runSeedClean(apiKey, baseUrl);
return;
Expand All @@ -51,7 +114,8 @@ export async function runSeed(
if (!options.file) {
return exitWithError({
code: 'missing_args',
message: 'Provide a seed file: workos seed --file=workos-seed.yml',
message:
'Provide a seed file: workos seed --file=workos-seed.yml\nRun workos seed --init to create an example seed file.',
});
}

Expand Down
8 changes: 8 additions & 0 deletions src/utils/help-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,14 @@ const commands: CommandSchema[] = [
default: false,
hidden: false,
},
{
name: 'init',
type: 'boolean',
description: 'Create an example workos-seed.yml file',
required: false,
default: false,
hidden: false,
},
],
},
{
Expand Down
Loading