@tenderprompt/cli 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +146 -8
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -60,6 +60,7 @@ tender --version
60
60
  tender capabilities --json
61
61
 
62
62
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
63
+ tender auth login --device --profile create --shopify-store tender-prompt --json
63
64
 
64
65
  tender app init artifact_123 --dir ./widget --preview --json
65
66
 
package/dist/index.js CHANGED
@@ -295,18 +295,21 @@ Commands:
295
295
  Examples:
296
296
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
297
297
  tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish
298
+ tender auth login --device --profile create --shopify-store tender-prompt --json
298
299
  tender auth status --json`;
299
300
  }
300
301
  function authLoginHelp() {
301
- return `Usage: tender auth login --device [--base-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll] [--json]
302
+ return `Usage: tender auth login --device [--base-url <url>] [--shopify-store <store>] [--shopify-app-url <url>] [--profile <edit-preview|publish|create|db-read>] [--artifact <artifact-id>] [--ttl <7d|14d|30d>] [--save-profile <name>] [--no-poll] [--json]
302
303
 
303
304
  Options:
304
305
  --device Use the OAuth device-code flow.
305
306
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL or https://app.tenderprompt.com.
307
+ --shopify-store <store> Approve the device code in Shopify Admin for this store handle.
308
+ --shopify-app-url <url> Approve the device code through a Tender Prompt Shopify App Home URL.
306
309
  --profile <value> Token permission profile: edit-preview, publish, create, or db-read. Defaults to edit-preview.
307
310
  --artifact <artifact-id> Scope the token to one artifact. Required for normal edit-preview agent work.
308
311
  --ttl <value> Token lifetime: 7d, 14d, or 30d. Defaults to 7d.
309
- --save-profile <name> Local config profile name. Defaults to default.
312
+ --save-profile <name> Local config profile name. Defaults to default, or shopify-<store> with Shopify approval.
310
313
  --no-poll Print the verification URL and exit without waiting for approval.
311
314
  --json Emit stable JSON for coding agents.
312
315
 
@@ -314,6 +317,7 @@ Examples:
314
317
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
315
318
  tender auth login --device --profile publish --artifact artifact_123 --ttl 7d --save-profile publish --json
316
319
  tender auth login --device --profile create --ttl 7d --save-profile account
320
+ tender auth login --device --profile create --shopify-store tender-prompt --json
317
321
  tender auth login --device --profile db-read --artifact artifact_123 --ttl 7d --save-profile db`;
318
322
  }
319
323
  function authStatusHelp() {
@@ -2033,8 +2037,38 @@ function normalizeStoredProfile(value) {
2033
2037
  permissions: Array.isArray(record.permissions)
2034
2038
  ? record.permissions.filter((permission) => typeof permission === 'string')
2035
2039
  : undefined,
2040
+ ...normalizeStoredShopifyContext(record),
2036
2041
  };
2037
2042
  }
2043
+ function normalizeStoredShopifyContext(record) {
2044
+ const shopifyAppUrl = typeof record.shopifyAppUrl === 'string' ? record.shopifyAppUrl : null;
2045
+ if (shopifyAppUrl) {
2046
+ try {
2047
+ const parsed = parseShopifyAppUrl(shopifyAppUrl, 'profile.shopifyAppUrl');
2048
+ return {
2049
+ shopifyAppUrl: parsed.appUrl,
2050
+ shopifyStore: parsed.store,
2051
+ };
2052
+ }
2053
+ catch {
2054
+ return {};
2055
+ }
2056
+ }
2057
+ const shopifyStore = typeof record.shopifyStore === 'string' ? record.shopifyStore : null;
2058
+ if (shopifyStore) {
2059
+ try {
2060
+ const store = parseShopifyStoreHandle(shopifyStore, 'profile.shopifyStore');
2061
+ return {
2062
+ shopifyAppUrl: shopifyAppUrlForStore(store),
2063
+ shopifyStore: store,
2064
+ };
2065
+ }
2066
+ catch {
2067
+ return {};
2068
+ }
2069
+ }
2070
+ return {};
2071
+ }
2038
2072
  function normalizeConfig(value) {
2039
2073
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
2040
2074
  return {};
@@ -2131,6 +2165,60 @@ function parseBaseUrlFlag(args, index) {
2131
2165
  }
2132
2166
  return normalizeBaseUrl(value);
2133
2167
  }
2168
+ function parseShopifyStoreHandle(value, flag) {
2169
+ if (!value || value.startsWith('-')) {
2170
+ throw new TenderCliUsageError(`${flag} requires a Shopify Admin store handle.`);
2171
+ }
2172
+ const normalized = value.trim().toLowerCase();
2173
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(normalized)) {
2174
+ throw new TenderCliUsageError(`${flag} must be a Shopify Admin store handle, such as tender-prompt.`);
2175
+ }
2176
+ return normalized;
2177
+ }
2178
+ function shopifyAppUrlForStore(store) {
2179
+ return `https://admin.shopify.com/store/${store}/apps/tender-prompt/shopify`;
2180
+ }
2181
+ function parseShopifyAppUrl(value, flag) {
2182
+ if (!value || value.startsWith('-')) {
2183
+ throw new TenderCliUsageError(`${flag} requires a Shopify Admin App Home URL.`);
2184
+ }
2185
+ let url;
2186
+ try {
2187
+ url = new URL(value);
2188
+ }
2189
+ catch {
2190
+ throw new TenderCliUsageError(`${flag} requires a valid Shopify Admin App Home URL.`);
2191
+ }
2192
+ if (url.protocol !== 'https:' || url.hostname !== 'admin.shopify.com') {
2193
+ throw new TenderCliUsageError(`${flag} must start with https://admin.shopify.com/.`);
2194
+ }
2195
+ const parts = url.pathname.split('/').filter(Boolean);
2196
+ const deviceSuffix = parts[5] === 'device' ? parts[5] : null;
2197
+ if ((parts.length !== 5 && !(parts.length === 6 && deviceSuffix)) ||
2198
+ parts[0] !== 'store' ||
2199
+ parts[2] !== 'apps' ||
2200
+ parts[4] !== 'shopify') {
2201
+ throw new TenderCliUsageError(`${flag} must look like https://admin.shopify.com/store/<store>/apps/tender-prompt/shopify.`);
2202
+ }
2203
+ if (url.search || url.hash) {
2204
+ throw new TenderCliUsageError(`${flag} should not include a query string or hash.`);
2205
+ }
2206
+ const store = parseShopifyStoreHandle(parts[1], flag);
2207
+ const appHandle = parseShopifyStoreHandle(parts[3], flag);
2208
+ return {
2209
+ store,
2210
+ appUrl: `https://admin.shopify.com/store/${store}/apps/${appHandle}/shopify`,
2211
+ };
2212
+ }
2213
+ function parseShopifyAppUrlFlag(args, index) {
2214
+ return parseShopifyAppUrl(args[index + 1], '--shopify-app-url');
2215
+ }
2216
+ function shopifyDeviceVerificationUri(shopifyAppUrl) {
2217
+ return `${shopifyAppUrl.replace(/\/+$/, '')}/device`;
2218
+ }
2219
+ function shopifyDeviceVerificationUrl(input) {
2220
+ return `${shopifyDeviceVerificationUri(input.shopifyAppUrl)}?user_code=${encodeURIComponent(input.userCode)}`;
2221
+ }
2134
2222
  function parseTokenFlag(args, index) {
2135
2223
  const value = args[index + 1];
2136
2224
  if (!value || value.startsWith('-')) {
@@ -4397,8 +4485,11 @@ function parseAuthLoginArgs(args) {
4397
4485
  }
4398
4486
  let device = false;
4399
4487
  let baseUrl = null;
4488
+ let shopifyAppUrl = null;
4489
+ let shopifyStore = null;
4400
4490
  let tokenProfile = 'edit-preview';
4401
4491
  let saveProfile = DEFAULT_PROFILE_NAME;
4492
+ let saveProfileExplicit = false;
4402
4493
  let artifactId = null;
4403
4494
  let ttl = '7d';
4404
4495
  let json = false;
@@ -4423,6 +4514,25 @@ function parseAuthLoginArgs(args) {
4423
4514
  index += 1;
4424
4515
  continue;
4425
4516
  }
4517
+ if (arg === '--shopify-store') {
4518
+ if (shopifyAppUrl) {
4519
+ throw new TenderCliUsageError('--shopify-store cannot be combined with --shopify-app-url.');
4520
+ }
4521
+ shopifyStore = parseShopifyStoreHandle(args[index + 1], '--shopify-store');
4522
+ shopifyAppUrl = shopifyAppUrlForStore(shopifyStore);
4523
+ index += 1;
4524
+ continue;
4525
+ }
4526
+ if (arg === '--shopify-app-url') {
4527
+ if (shopifyAppUrl) {
4528
+ throw new TenderCliUsageError('--shopify-app-url cannot be combined with --shopify-store.');
4529
+ }
4530
+ const parsed = parseShopifyAppUrlFlag(args, index);
4531
+ shopifyStore = parsed.store;
4532
+ shopifyAppUrl = parsed.appUrl;
4533
+ index += 1;
4534
+ continue;
4535
+ }
4426
4536
  if (arg === '--profile') {
4427
4537
  tokenProfile = parseTokenProfile(args[index + 1]);
4428
4538
  index += 1;
@@ -4430,6 +4540,7 @@ function parseAuthLoginArgs(args) {
4430
4540
  }
4431
4541
  if (arg === '--save-profile') {
4432
4542
  saveProfile = parseProfileName(args[index + 1], '--save-profile');
4543
+ saveProfileExplicit = true;
4433
4544
  index += 1;
4434
4545
  continue;
4435
4546
  }
@@ -4459,9 +4570,14 @@ function parseAuthLoginArgs(args) {
4459
4570
  if (tokenProfile === 'db-read' && !artifactId) {
4460
4571
  throw new TenderCliUsageError('--profile db-read requires --artifact <artifact-id>.');
4461
4572
  }
4573
+ if (shopifyStore && !saveProfileExplicit) {
4574
+ saveProfile = `shopify-${shopifyStore}`;
4575
+ }
4462
4576
  return {
4463
4577
  command: 'auth login',
4464
4578
  baseUrl,
4579
+ shopifyAppUrl,
4580
+ shopifyStore,
4465
4581
  device,
4466
4582
  tokenProfile,
4467
4583
  saveProfile,
@@ -8145,6 +8261,8 @@ async function runAuthStatus(command, io, runtime) {
8145
8261
  artifactId: profile?.artifactId ?? null,
8146
8262
  permissions: profile?.permissions ?? [],
8147
8263
  expiresAt: formatExpiry(profile?.expiresAt),
8264
+ shopifyAppUrl: profile?.shopifyAppUrl ?? null,
8265
+ shopifyStore: profile?.shopifyStore ?? null,
8148
8266
  };
8149
8267
  if (command.json) {
8150
8268
  io.stdout(JSON.stringify(result, null, 2));
@@ -8159,6 +8277,7 @@ async function runAuthStatus(command, io, runtime) {
8159
8277
  `app_id: ${profile.artifactId ?? '(account)'}`,
8160
8278
  `permissions: ${(profile.permissions ?? []).join(' ') || '(unknown)'}`,
8161
8279
  `expires_at: ${formatExpiry(profile.expiresAt) ?? '(unknown)'}`,
8280
+ `shopify_app_url: ${profile.shopifyAppUrl ?? '(none)'}`,
8162
8281
  ].join('\n'));
8163
8282
  }
8164
8283
  else {
@@ -8203,6 +8322,8 @@ async function runAuthStatus(command, io, runtime) {
8203
8322
  artifactId: profile?.artifactId ?? null,
8204
8323
  permissions: profile?.permissions ?? [],
8205
8324
  expiresAt: formatExpiry(profile?.expiresAt),
8325
+ shopifyAppUrl: profile?.shopifyAppUrl ?? null,
8326
+ shopifyStore: profile?.shopifyStore ?? null,
8206
8327
  };
8207
8328
  if (command.json) {
8208
8329
  io.stdout(JSON.stringify(result, null, 2));
@@ -8217,6 +8338,7 @@ async function runAuthStatus(command, io, runtime) {
8217
8338
  `app_id: ${profile.artifactId ?? '(account)'}`,
8218
8339
  `permissions: ${(profile.permissions ?? []).join(' ') || '(unknown)'}`,
8219
8340
  `expires_at: ${formatExpiry(profile.expiresAt) ?? '(unknown)'}`,
8341
+ `shopify_app_url: ${profile.shopifyAppUrl ?? '(none)'}`,
8220
8342
  ].join('\n'));
8221
8343
  }
8222
8344
  else {
@@ -8233,7 +8355,11 @@ async function runAuthLogin(command, io, runtime) {
8233
8355
  const fetcher = runtime.fetcher ?? fetch;
8234
8356
  const sleeper = runtime.sleeper ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
8235
8357
  const now = runtime.now ?? Date.now;
8236
- const baseUrl = command.baseUrl ?? readDefaultBaseUrl(env);
8358
+ const config = await readConfig(env);
8359
+ const existingProfile = config.profiles?.[command.saveProfile] ?? null;
8360
+ const baseUrl = command.baseUrl ?? existingProfile?.baseUrl ?? readDefaultBaseUrl(env);
8361
+ const shopifyAppUrl = command.shopifyAppUrl ?? existingProfile?.shopifyAppUrl ?? null;
8362
+ const shopifyStore = command.shopifyStore ?? existingProfile?.shopifyStore ?? null;
8237
8363
  const deviceResponse = await fetcher(`${baseUrl}/oauth/device/code`, {
8238
8364
  method: 'POST',
8239
8365
  headers: {
@@ -8249,15 +8375,24 @@ async function runAuthLogin(command, io, runtime) {
8249
8375
  if (!deviceResponse.ok || !devicePayload?.device_code || !devicePayload.user_code || !devicePayload.verification_uri_complete) {
8250
8376
  throw new Error(devicePayload?.error_description ?? devicePayload?.error ?? `Device authorization failed with HTTP ${deviceResponse.status}.`);
8251
8377
  }
8378
+ const verificationUriComplete = shopifyAppUrl
8379
+ ? shopifyDeviceVerificationUrl({
8380
+ shopifyAppUrl,
8381
+ userCode: devicePayload.user_code,
8382
+ })
8383
+ : devicePayload.verification_uri_complete;
8384
+ const verificationUri = shopifyAppUrl ? shopifyDeviceVerificationUri(shopifyAppUrl) : devicePayload.verification_uri;
8252
8385
  const pendingResult = {
8253
8386
  ok: true,
8254
8387
  command: command.command,
8255
8388
  baseUrl,
8389
+ shopifyAppUrl,
8390
+ shopifyStore,
8256
8391
  profile: command.tokenProfile,
8257
8392
  saveProfile: command.saveProfile,
8258
8393
  userCode: devicePayload.user_code,
8259
- verificationUri: devicePayload.verification_uri,
8260
- verificationUriComplete: devicePayload.verification_uri_complete,
8394
+ verificationUri,
8395
+ verificationUriComplete,
8261
8396
  expiresIn: devicePayload.expires_in ?? null,
8262
8397
  interval: devicePayload.interval ?? null,
8263
8398
  };
@@ -8266,13 +8401,13 @@ async function runAuthLogin(command, io, runtime) {
8266
8401
  io.stdout(JSON.stringify(pendingResult, null, 2));
8267
8402
  }
8268
8403
  else {
8269
- io.stdout([`verification_url: ${devicePayload.verification_uri_complete}`, `user_code: ${devicePayload.user_code}`].join('\n'));
8404
+ io.stdout([`verification_url: ${verificationUriComplete}`, `user_code: ${devicePayload.user_code}`].join('\n'));
8270
8405
  }
8271
8406
  return 0;
8272
8407
  }
8273
8408
  if (!command.json) {
8274
8409
  io.stdout([
8275
- `Open this URL to approve Tender CLI access: ${devicePayload.verification_uri_complete}`,
8410
+ `Open this URL to approve Tender CLI access: ${verificationUriComplete}`,
8276
8411
  `User code: ${devicePayload.user_code}`,
8277
8412
  ].join('\n'));
8278
8413
  }
@@ -8293,7 +8428,6 @@ async function runAuthLogin(command, io, runtime) {
8293
8428
  });
8294
8429
  const tokenPayload = await parseJsonResponse(tokenResponse);
8295
8430
  if (tokenResponse.ok && tokenPayload?.access_token) {
8296
- const config = await readConfig(env);
8297
8431
  const profiles = config.profiles ?? {};
8298
8432
  profiles[command.saveProfile] = {
8299
8433
  baseUrl,
@@ -8303,6 +8437,8 @@ async function runAuthLogin(command, io, runtime) {
8303
8437
  artifactId: tokenPayload.artifact_id,
8304
8438
  expiresAt: tokenPayload.expires_at,
8305
8439
  permissions: splitScope(tokenPayload.scope),
8440
+ ...(shopifyAppUrl ? { shopifyAppUrl } : {}),
8441
+ ...(shopifyStore ? { shopifyStore } : {}),
8306
8442
  };
8307
8443
  await writeConfig(env, {
8308
8444
  ...config,
@@ -8320,6 +8456,8 @@ async function runAuthLogin(command, io, runtime) {
8320
8456
  artifactId: tokenPayload.artifact_id ?? null,
8321
8457
  permissions: splitScope(tokenPayload.scope),
8322
8458
  expiresAt: formatExpiry(tokenPayload.expires_at),
8459
+ shopifyAppUrl,
8460
+ shopifyStore,
8323
8461
  configPath: readConfigPath(env),
8324
8462
  };
8325
8463
  if (command.json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenderprompt/cli",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tender": "bin/tender.js"
@@ -23,7 +23,7 @@
23
23
  "prepack": "yarn build"
24
24
  },
25
25
  "dependencies": {
26
- "@tenderprompt/tender-app-validator": "0.1.4"
26
+ "@tenderprompt/tender-app-validator": "0.1.5"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=20"