Skip to content
Draft
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
11,471 changes: 5,734 additions & 5,737 deletions packages/cli/oclif.manifest.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@
"store": {
"description": "Work directly with Shopify stores."
},
"preview": {
"description": "Spin up and claim Preview Stores backed by a placeholder identity (prototype)."
},
"app:config": {
"description": "Manage app configuration."
},
Expand Down
59 changes: 59 additions & 0 deletions packages/cli/src/cli/commands/preview/claim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {claimPreviewStoreCommand} from '../../services/commands/preview/claim.js'
import Command from '@shopify/cli-kit/node/base-command'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
import {Flags} from '@oclif/core'

export default class PreviewStoreClaim extends Command {
static summary = 'Generate a claim URL that transfers a Preview Store to a real merchant identity.'

static description = `Calls Core's /services/preview-stores/claim endpoint, which wraps the existing org-based vibe transfer flow used today by Lovable. Returns a claim URL the recipient opens to take ownership of the store.`

static examples = [
'<%= config.bin %> <%= command.id %> --shop-id 21 --recipient-email merchant@example.com',
]

static flags = {
...globalFlags,
...jsonFlag,
'shop-id': Flags.integer({
description: 'Numeric shop id returned by `preview create`.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_SHOP_ID',
required: true,
}),
'recipient-email': Flags.string({
description: 'Email of the merchant identity that should take ownership after they accept the claim link.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_RECIPIENT_EMAIL',
required: true,
}),
'core-url': Flags.string({
description: 'Base URL of the Core orchestrator. Defaults to https://app.shop.dev.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_CORE_URL',
required: false,
}),
'cli-username': Flags.string({
description: 'Basic-auth username for the Core endpoint. Defaults to "preview-store-cli".',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_CLI_USERNAME',
required: false,
}),
'cli-secret': Flags.string({
description: 'Basic-auth secret for the Core endpoint. Defaults to the dev value "preview-store-cli-dev".',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_CLI_SECRET',
required: false,
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(PreviewStoreClaim)

await claimPreviewStoreCommand({
shopId: flags['shop-id'],
recipientEmail: flags['recipient-email'],
json: Boolean(flags.json),
client: {
coreUrl: flags['core-url'],
cliUsername: flags['cli-username'],
cliSecret: flags['cli-secret'],
},
})
}
}
72 changes: 72 additions & 0 deletions packages/cli/src/cli/commands/preview/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {createPreviewStoreCommand} from '../../services/commands/preview/create.js'
import Command from '@shopify/cli-kit/node/base-command'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
import {Flags} from '@oclif/core'

export default class PreviewStoreCreate extends Command {
static summary = 'Create a Preview Store backed by a placeholder identity.'

static description = `Calls Core's /services/preview-stores orchestrator. Returns a shop, an Admin API token, and a one-time-use magic-link URL the user can open to land in admin without an Identity login.

Targets a local Core rig by default (https://app.shop.dev). Override --core-url to point elsewhere.`

static examples = [
'<%= config.bin %> <%= command.id %> --shop-name my-preview',
'<%= config.bin %> <%= command.id %> --shop-name my-preview --email demo@previewstore.invalid',
'<%= config.bin %> <%= command.id %> --shop-name my-preview --json',
]

static flags = {
...globalFlags,
...jsonFlag,
'shop-name': Flags.string({
char: 'n',
description: 'Subdomain prefix for the new preview store (e.g. "my-preview"). Auto-generated if omitted.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_SHOP_NAME',
required: false,
}),
email: Flags.string({
description: 'Email to associate with the placeholder identity. Defaults to a generated @previewstore.invalid address.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_EMAIL',
required: false,
}),
country: Flags.string({
description: 'ISO country code for the new store. Defaults to US.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_COUNTRY',
required: false,
}),
'core-url': Flags.string({
description: 'Base URL of the Core orchestrator. Defaults to https://app.shop.dev.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_CORE_URL',
required: false,
}),
'cli-username': Flags.string({
description: 'Basic-auth username for the Core endpoint. Defaults to "preview-store-cli".',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_CLI_USERNAME',
required: false,
}),
'cli-secret': Flags.string({
description: 'Basic-auth secret for the Core endpoint. Defaults to the dev value "preview-store-cli-dev".',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_CLI_SECRET',
required: false,
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(PreviewStoreCreate)

const shopName = flags['shop-name'] ?? `preview-${Math.floor(Date.now() / 1000)}`

await createPreviewStoreCommand({
shopName,
email: flags.email,
country: flags.country,
json: Boolean(flags.json),
client: {
coreUrl: flags['core-url'],
cliUsername: flags['cli-username'],
cliSecret: flags['cli-secret'],
},
})
}
}
94 changes: 94 additions & 0 deletions packages/cli/src/cli/commands/preview/execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {executePreviewStoreCommand} from '../../services/commands/preview/execute.js'
import Command from '@shopify/cli-kit/node/base-command'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
import {Flags} from '@oclif/core'

export default class PreviewStoreExecute extends Command {
static summary = 'Run an Admin GraphQL operation against a Preview Store using its shop-scoped token.'

static description = `Reads the shop domain and admin API token straight from \`preview create --json\` output (or accepts them as flags). Calls the Admin GraphQL endpoint at the requested API version. Mutations are blocked unless --allow-mutations is set.`

static examples = [
'<%= config.bin %> <%= command.id %> --from-file /tmp/preview.json --query "{ shop { name } }"',
'<%= config.bin %> <%= command.id %> --from-file /tmp/preview.json --query-file ./query.graphql',
'<%= config.bin %> <%= command.id %> --domain shop.myshopify.io --token shpat_... --query "..."',
'<%= config.bin %> <%= command.id %> --from-file /tmp/preview.json --allow-mutations --query-file ./mutation.graphql',
]

static flags = {
...globalFlags,
...jsonFlag,
'from-file': Flags.string({
description: 'Path to JSON produced by `preview create --json`. Provides domain + admin token.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_FROM_FILE',
required: false,
exclusive: ['domain', 'token'],
}),
domain: Flags.string({
description: 'Permanent shop domain (e.g. preview-123.myshopify.io). Required if --from-file is omitted.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_DOMAIN',
required: false,
dependsOn: ['token'],
}),
token: Flags.string({
description: 'Admin API token (shpat_...). Required if --from-file is omitted.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_TOKEN',
required: false,
dependsOn: ['domain'],
}),
query: Flags.string({
char: 'q',
description: 'GraphQL query or mutation as a string.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_QUERY',
required: false,
exclusive: ['query-file'],
}),
'query-file': Flags.string({
description: 'Path to a file containing a GraphQL query or mutation.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_QUERY_FILE',
required: false,
exclusive: ['query'],
}),
variables: Flags.string({
description: 'GraphQL variables as a JSON string.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_VARIABLES',
required: false,
exclusive: ['variable-file'],
}),
'variable-file': Flags.string({
description: 'Path to a JSON file containing GraphQL variables.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_VARIABLE_FILE',
required: false,
exclusive: ['variables'],
}),
'api-version': Flags.string({
description: 'Admin API version. Defaults to "unstable".',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_API_VERSION',
required: false,
default: 'unstable',
}),
'allow-mutations': Flags.boolean({
description: 'Allow mutation operations. Required for any GraphQL document with `mutation`.',
env: 'SHOPIFY_FLAG_PREVIEW_STORE_ALLOW_MUTATIONS',
required: false,
default: false,
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(PreviewStoreExecute)

await executePreviewStoreCommand({
fromFile: flags['from-file'],
domain: flags.domain,
token: flags.token,
query: flags.query,
queryFile: flags['query-file'],
variables: flags.variables,
variableFile: flags['variable-file'],
apiVersion: flags['api-version'],
allowMutations: flags['allow-mutations'],
json: Boolean(flags.json),
})
}
}
45 changes: 45 additions & 0 deletions packages/cli/src/cli/services/commands/preview/claim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
PreviewStoreClaimResponse,
PreviewStoreClientOptions,
claimPreviewStore,
defaultClientOptions,
} from './client.js'
import {outputResult} from '@shopify/cli-kit/node/output'
import {renderSuccess} from '@shopify/cli-kit/node/ui'

export interface ClaimPreviewStoreInput {
shopId: number
recipientEmail: string
json: boolean
client?: Partial<PreviewStoreClientOptions>
}

export async function claimPreviewStoreCommand(input: ClaimPreviewStoreInput): Promise<void> {
const options = defaultClientOptions(input.client)
const response = await claimPreviewStore(
{shop_id: input.shopId, email: input.recipientEmail},
options,
)

if (input.json) {
outputResult(JSON.stringify(response, null, 2))
return
}

renderResponse(response)
}

function renderResponse(response: PreviewStoreClaimResponse): void {
renderSuccess({
headline: 'Claim link ready.',
customSections: [
{
title: 'Claim URL',
body: response.claim_store_url,
},
],
nextSteps: [
['Share the claim URL with the recipient. Opening it transfers the store to their identity through the existing org-based vibe transfer flow.'],
],
})
}
Loading
Loading