Skip to content
Open
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
101 changes: 101 additions & 0 deletions src/commands/console/project/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
Copyright 2026 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
// const aioConsoleLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-console:project:create', { provider: 'debug' })
const { Flags } = require('@oclif/core')
const ConsoleCommand = require('../index')

class CreateCommand extends ConsoleCommand {
async run () {
const { flags } = await this.parse(CreateCommand)
const orgId = flags.orgId || this.getConfig('org.id')
if (!orgId) {
this.error('You have not selected an Organization. Please select first.')
}

const projectDetails = {
name: flags.name,
title: flags.title || flags.name,
description: flags.description || flags.name
}

// first validate name, before calling server to check if it is already in use
// Project name allows only alphanumeric values
if (!/^[a-zA-Z0-9]+$/.test(projectDetails.name)) {
this.error(`Project name ${projectDetails.name} is invalid. It should only contain alphanumeric values.`)
}
// Project name must be between 3 and 45 characters long.
if (projectDetails.name.length < 3 || projectDetails.name.length > 45) {
this.error('Project name must be between 3 and 45 characters long.')
}

// Project title should only contain English alphanumeric or Latin alphabet characters and spaces.
if (!/^[a-zA-Z0-9\s]+$/.test(projectDetails.title)) {
this.error(`Project title ${projectDetails.title} is invalid. It should only contain English alphanumeric or Latin alphabet characters and spaces.`)
}
// Project title must be between 3 and 45 characters long.
if (projectDetails.title.length < 3 || projectDetails.title.length > 45) {
this.error('Project title must be between 3 and 45 characters long.')
}
// Description cannot be over 1000 characters.
if (projectDetails.description.length > 1000) {
this.error('Project description cannot be over 1000 characters.')
}

await this.initSdk()
// check name is not already in use
const projects = await this.consoleCLI.getProjects(orgId)
if (projects.find(project => project.name === projectDetails.name)) {
this.error(`Project ${projectDetails.name} already exists. Please choose a different name.`)
}

// if we get here, all validation passed, so call server to create project
const project = await this.consoleCLI.createProject(orgId, projectDetails)
this.log(`Project ${project.name} created successfully.`)
return project
}
}

CreateCommand.description = 'Create a new App Builder Project for the selected Organization'

CreateCommand.flags = {
...ConsoleCommand.flags,
orgId: Flags.string({
description: 'OrgID to create the project in'
}),
name: Flags.string({
description: 'Name of the project',
required: true
}),
title: Flags.string({
description: 'Title of the project, defaults to the name'
}),
description: Flags.string({
description: 'Description of the project, defaults to the name'
}),
json: Flags.boolean({
description: 'Output json',
char: 'j',
exclusive: ['yml']
}),
yml: Flags.boolean({
description: 'Output yml',
char: 'y',
exclusive: ['json']
})
}

CreateCommand.aliases = [
'console:project:create',
'console:project:init'
]

module.exports = CreateCommand
108 changes: 108 additions & 0 deletions src/commands/console/workspace/create.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
Copyright 2026 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/
const { Flags } = require('@oclif/core')
const ConsoleCommand = require('../index')

class CreateCommand extends ConsoleCommand {
async run () {
const { flags } = await this.parse(CreateCommand)
const orgId = flags.orgId || this.getConfig('org.id')
if (!orgId) {
this.error('You have not selected an Organization. Please select first.')
}

const workspaceDetails = {
name: flags.name,
title: flags.title || flags.name
}

// Workspace name allows only alphanumeric values
if (!/^[a-zA-Z0-9]+$/.test(workspaceDetails.name)) {
this.error(`Workspace name ${workspaceDetails.name} is invalid. It should only contain alphanumeric values.`)
}
if (workspaceDetails.name.length < 3 || workspaceDetails.name.length > 45) {
this.error('Workspace name must be between 3 and 45 characters long.')
}

// Workspace title should only contain alphanumeric characters and spaces
if (!/^[a-zA-Z0-9\s]+$/.test(workspaceDetails.title)) {
this.error(`Workspace title ${workspaceDetails.title} is invalid. It should only contain alphanumeric characters and spaces.`)
}
if (workspaceDetails.title.length < 3 || workspaceDetails.title.length > 45) {
this.error('Workspace title must be between 3 and 45 characters long.')
}

await this.initSdk()

try {
// resolve project by name or title to project id
const projects = await this.consoleCLI.getProjects(orgId)
const project = projects.find(p => p.name === flags.projectName || p.title === flags.projectName)
if (!project) {
this.error(`Project ${flags.projectName} not found in the Organization.`)
}
const projectId = project.id

const workspaces = await this.consoleCLI.getWorkspaces(orgId, projectId)
if (workspaces.find(ws => ws.name === workspaceDetails.name)) {
this.error(`Workspace ${workspaceDetails.name} already exists. Please choose a different name.`)
}

const workspace = await this.consoleCLI.createWorkspace(orgId, projectId, workspaceDetails)
this.log(`Workspace ${workspace.name} created successfully.`)
return workspace
} catch (err) {
this.error(err.message)
} finally {
this.cleanOutput()
}
}
}

CreateCommand.description = 'Create a new Workspace for the selected Project'

CreateCommand.flags = {
...ConsoleCommand.flags,
orgId: Flags.string({
description: 'OrgID of the project to create the workspace in'
}),
projectName: Flags.string({
description: 'Name or title of the project to create the workspace in',
required: true
}),
name: Flags.string({
description: 'Name of the workspace',
required: true
}),
title: Flags.string({
description: 'Title of the workspace, defaults to the name'
}),
json: Flags.boolean({
description: 'Output json',
char: 'j',
exclusive: ['yml']
}),
yml: Flags.boolean({
description: 'Output yml',
char: 'y',
exclusive: ['json']
})
}

CreateCommand.aliases = [
'console:workspace:create',
'console:workspace:init',
'console:ws:create',
'console:ws:init'
]

module.exports = CreateCommand
158 changes: 158 additions & 0 deletions test/commands/console/project/create.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
Copyright 2026 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const mockProject = {
appId: null,
date_created: '2020-04-29T10:14:17.000Z',
date_last_modified: '2020-04-29T10:14:17.000Z',
deleted: 0,
description: 'Description 1',
id: '1000000001',
name: 'name1',
org_id: 1234567890,
title: 'Title 1',
type: 'default'
}

const mockConsoleCLIInstance = {
getProjects: jest.fn().mockResolvedValue([]),
createProject: jest.fn().mockResolvedValue(mockProject)
}

jest.mock('@adobe/aio-cli-lib-console', () => ({
init: jest.fn().mockResolvedValue(mockConsoleCLIInstance),
cleanStdOut: jest.fn()
}))

const TheCommand = require('../../../../src/commands/console/project/create')

describe('console:project:create', () => {
let command

beforeEach(() => {
command = new TheCommand()
mockConsoleCLIInstance.createProject.mockReset()
})

afterEach(() => {
command = null
})

it('should create a project', async () => {
mockConsoleCLIInstance.createProject.mockResolvedValue(mockProject)
command.argv = ['--name', 'testproject', '--title', 'Test Project', '--description', 'Test Project Description', '--orgId', '1234567890']
const result = await command.run()
expect(mockConsoleCLIInstance.createProject).toHaveBeenCalledWith('1234567890', {
name: 'testproject',
title: 'Test Project',
description: 'Test Project Description'
})
expect(result).toEqual(mockProject)
})

it('should not create a project if the name is not provided', async () => {
command.argv = ['--title', 'Test Project', '--description', 'Test Project Description', '--orgId', '1234567890']
await expect(command.run()).rejects.toThrow('Missing required flag name')
expect(mockConsoleCLIInstance.createProject).not.toHaveBeenCalled()
})

it('should not create a project if the orgId is not provided', async () => {
command.argv = ['--name', 'testproject', '--title', 'Test Project', '--description', 'Test Project Description']
command.getConfig = jest.fn().mockReturnValue(null)
await expect(command.run()).rejects.toThrow('You have not selected an Organization. Please select first.')
expect(mockConsoleCLIInstance.createProject).not.toHaveBeenCalled()
})

it('should use config.org.id if no orgId is provided', async () => {
mockConsoleCLIInstance.createProject.mockResolvedValue(mockProject)
command.argv = ['--name', 'testproject', '--title', 'Test Project', '--description', 'Test Project Description']
command.getConfig = jest.fn().mockReturnValue('0987654321')
const result = await command.run()
expect(mockConsoleCLIInstance.createProject).toHaveBeenCalledWith('0987654321', {
name: 'testproject',
title: 'Test Project',
description: 'Test Project Description'
})
expect(result).toEqual(mockProject)
})

it('should not create a project if the name is already in use', async () => {
mockConsoleCLIInstance.getProjects.mockResolvedValue([mockProject])
mockConsoleCLIInstance.createProject.mockResolvedValue(mockProject)
command.argv = ['--name', 'name1', '--title', 'Test Project', '--description', 'Test Project Description', '--orgId', '1234567890']
await expect(command.run()).rejects.toThrow('Project name1 already exists. Please choose a different name.')
expect(mockConsoleCLIInstance.createProject).not.toHaveBeenCalled()
})

it('should use name as title if no title is provided', async () => {
mockConsoleCLIInstance.getProjects.mockResolvedValue([])
mockConsoleCLIInstance.createProject.mockResolvedValue(mockProject)
command.argv = ['--name', 'testproject', '--description', 'Test Project Description', '--orgId', '1234567890']
const result = await command.run()
expect(mockConsoleCLIInstance.createProject).toHaveBeenCalledWith('1234567890', {
name: 'testproject',
title: 'testproject',
description: 'Test Project Description'
})
expect(result).toEqual(mockProject)
})

it('should use name as description if no description is provided', async () => {
mockConsoleCLIInstance.getProjects.mockResolvedValue([])
mockConsoleCLIInstance.createProject.mockResolvedValue(mockProject)
command.argv = ['--name', 'testproject', '--title', 'Test Project', '--orgId', '1234567890']
const result = await command.run()
expect(mockConsoleCLIInstance.createProject).toHaveBeenCalledWith('1234567890', {
name: 'testproject',
title: 'Test Project',
description: 'testproject'
})
expect(result).toEqual(mockProject)
})

it('should not create a project if the name is invalid', async () => {
command.argv = ['--name', 'test-project!', '--title', 'Test Project', '--description', 'Test Project Description', '--orgId', '1234567890']
await expect(command.run()).rejects.toThrow('Project name test-project! is invalid. It should only contain alphanumeric values.')
expect(mockConsoleCLIInstance.createProject).not.toHaveBeenCalled()
})

it('should not create a project if the title is invalid', async () => {
command.argv = ['--name', 'testproject', '--title', 'Test Project!', '--description', 'Test Project Description', '--orgId', '1234567890']
await expect(command.run()).rejects.toThrow('Project title Test Project! is invalid. It should only contain English alphanumeric or Latin alphabet characters and spaces.')
expect(mockConsoleCLIInstance.createProject).not.toHaveBeenCalled()
})

it('should not create a project if the description is too long', async () => {
command.argv = ['--name', 'testproject', '--title', 'Test Project', '--description', 'Test Project Description'.repeat(1000), '--orgId', '1234567890']
await expect(command.run()).rejects.toThrow('Project description cannot be over 1000 characters.')
expect(mockConsoleCLIInstance.createProject).not.toHaveBeenCalled()
})

it('should not create a project if the name is too long, or too short', async () => {
command.argv = ['--name', 'testproject'.repeat(50), '--title', 'Test Project', '--description', 'Test Project Description', '--orgId', '1234567890']
await expect(command.run()).rejects.toThrow('Project name must be between 3 and 45 characters long.')
expect(mockConsoleCLIInstance.createProject).not.toHaveBeenCalled()

command.argv = ['--name', '1', '--orgId', '1234567890']
await expect(command.run()).rejects.toThrow('Project name must be between 3 and 45 characters long.')
expect(mockConsoleCLIInstance.createProject).not.toHaveBeenCalled()
})

it('should not create a project if the title is too long or too short', async () => {
command.argv = ['--name', 'testName', '--title', 'Test Project'.repeat(50), '--description', 'Test Project Description', '--orgId', '1234567890']
await expect(command.run()).rejects.toThrow('Project title must be between 3 and 45 characters long.')
expect(mockConsoleCLIInstance.createProject).not.toHaveBeenCalled()

command.argv = ['--name', 'testName', '--title', 'HI', '--orgId', '1234567890']
await expect(command.run()).rejects.toThrow('Project title must be between 3 and 45 characters long.')
expect(mockConsoleCLIInstance.createProject).not.toHaveBeenCalled()
})
})
Loading
Loading