@promptframe/cli 0.1.31 → 0.1.33

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.
package/dist/index.js CHANGED
@@ -20,6 +20,20 @@ const SECURITY_EVALUATOR_MODE = 'ast';
20
20
  const LOCKFILE_EVIDENCE_SCHEMA_VERSION = 'promptframe.lockfile-evidence.v0.1.0';
21
21
  const LOCKFILE_EVIDENCE_FILE_NAME = 'promptframe-lockfile-evidence.json';
22
22
  const PROMPTFRAME_WORKSPACE_SCHEMA_VERSION = 'promptframe-workspace.v0.1.0';
23
+ const PROMPTFRAME_PROJECT_CONTEXT_SCHEMA_VERSION = 'promptframe-project-context.v0.1.0';
24
+ const PROMPTFRAME_PROJECT_CONTEXT_FILE_NAME = '.promptframerc';
25
+ const PROJECT_CONTEXT_FORBIDDEN_KEY_NAMES = new Set([
26
+ 'apikey',
27
+ 'auth0subject',
28
+ 'authorization',
29
+ 'citokensecret',
30
+ 'clientsecret',
31
+ 'cookie',
32
+ 'oauthcode',
33
+ 'password',
34
+ 'token',
35
+ 'tokensecret',
36
+ ]);
23
37
  const PACKAGE_MANAGER_LOCKFILE_NAMES = [
24
38
  'package-lock.json',
25
39
  'npm-shrinkwrap.json',
@@ -84,12 +98,19 @@ async function run(name, argv) {
84
98
  case 'discovery':
85
99
  await discovery(argv);
86
100
  break;
101
+ case 'init':
102
+ await initProjectContext(argv);
103
+ break;
87
104
  case 'project':
88
105
  await project(argv);
89
106
  break;
90
107
  case 'projects':
91
108
  await project(['list', ...argv]);
92
109
  break;
110
+ case 'component':
111
+ case 'components':
112
+ await component(argv);
113
+ break;
93
114
  case 'ci-token':
94
115
  case 'token':
95
116
  await ciToken(argv);
@@ -406,14 +427,7 @@ function evaluateDirectoryDependencyPolicy(dir) {
406
427
  });
407
428
  }
408
429
  function hasPackageManagerLockfile(dir) {
409
- return [
410
- 'pnpm-lock.yaml',
411
- 'package-lock.json',
412
- 'npm-shrinkwrap.json',
413
- 'yarn.lock',
414
- 'bun.lock',
415
- 'bun.lockb',
416
- ].some((fileName) => existsSync(join(dir, fileName)));
430
+ return collectPackageManagerLockfileEvidence(dir).length > 0;
417
431
  }
418
432
  function assertDependencyPolicyAccepted(receipt) {
419
433
  if (receipt.status !== 'reject')
@@ -495,8 +509,9 @@ async function uploadComponent(argv) {
495
509
  'x-promptframe-upload-target': uploadTarget,
496
510
  },
497
511
  }, 'upload.http.failed');
498
- const output = {
499
- ...payload,
512
+ const safePayload = sanitizePublicUploadPayload(payload);
513
+ const output = sanitizePublicUploadPayload({
514
+ ...safePayload,
500
515
  command: 'upload',
501
516
  endpoint,
502
517
  uploadTarget,
@@ -505,7 +520,7 @@ async function uploadComponent(argv) {
505
520
  source: input.source,
506
521
  ...(localReusability ? { localReusability, diagnostics: localDiagnostics } : {}),
507
522
  diagnostic: diagnostic('upload.completed', 'info', 'Component upload accepted by platform.'),
508
- };
523
+ });
509
524
  if (hasFlag(argv, '--json')) {
510
525
  printJson(output);
511
526
  return;
@@ -513,10 +528,56 @@ async function uploadComponent(argv) {
513
528
  console.log('Upload accepted by PromptFrame platform.');
514
529
  console.log(`Build: ${output.jobId ?? 'unknown'}`);
515
530
  console.log(`Status: ${stringValue(payload.status) ?? stringValue(asRecord(payload.build)?.status) ?? 'queued'}`);
531
+ if (output.jobId) {
532
+ console.log(`Next: promptframe status ${output.jobId} --endpoint ${endpoint}`);
533
+ }
516
534
  if (localDiagnostics)
517
535
  printDiagnostics(localDiagnostics);
518
536
  printStatusUrl(endpoint, payload);
519
537
  }
538
+ const PUBLIC_UPLOAD_JSON_OMIT_KEYS = new Set([
539
+ 'providerUsageReceipt',
540
+ 'providerUsageReceipts',
541
+ 'vectorRef',
542
+ 'retryKey',
543
+ 'sourceHash',
544
+ 'sourceHashDetails',
545
+ 'internalSourceHash',
546
+ 'schemaHash',
547
+ 'manifestHash',
548
+ 'bundleHash',
549
+ 'evidence',
550
+ 'evidenceRecords',
551
+ 'evidenceItems',
552
+ 'rawEvidence',
553
+ ]);
554
+ function sanitizePublicUploadPayload(value) {
555
+ return asRecord(sanitizePublicUploadValue(value)) ?? {};
556
+ }
557
+ function sanitizePublicUploadValue(value) {
558
+ if (typeof value === 'string') {
559
+ return scrubInternalReferences(value);
560
+ }
561
+ if (Array.isArray(value)) {
562
+ return value.map((item) => sanitizePublicUploadValue(item));
563
+ }
564
+ const record = asRecord(value);
565
+ if (!record)
566
+ return value;
567
+ const output = {};
568
+ for (const [key, entry] of Object.entries(record)) {
569
+ if (PUBLIC_UPLOAD_JSON_OMIT_KEYS.has(key))
570
+ continue;
571
+ output[key] = sanitizePublicUploadValue(entry);
572
+ }
573
+ return output;
574
+ }
575
+ function scrubInternalReferences(value) {
576
+ return value
577
+ .replace(/\bREQ-\d+\b/g, 'internal reference')
578
+ .replace(/\bTASK-\d+(?:-[A-Za-z0-9-]+)?\b/g, 'internal task')
579
+ .replace(/\bBUG-\d+\b/g, 'internal bug');
580
+ }
520
581
  function resolveUploadTarget(argv) {
521
582
  const raw = normalizeUploadTargetAlias(valueAfter(argv, '--target') ?? valueAfter(argv, '--upload-target') ?? 'marketplace_authoring');
522
583
  const parsed = authoringUploadTargetSchema.safeParse(raw);
@@ -874,7 +935,10 @@ const POSITIONAL_VALUE_FLAGS = new Set([
874
935
  '--auth-permissions',
875
936
  '--auth-roles',
876
937
  '--component-path',
938
+ '--component-id',
877
939
  '--config',
940
+ '--description',
941
+ '--display-name',
878
942
  '--endpoint',
879
943
  '--expires-at',
880
944
  '--host',
@@ -887,6 +951,9 @@ const POSITIONAL_VALUE_FLAGS = new Set([
887
951
  '--provider',
888
952
  '--provider-kind',
889
953
  '--provider-name',
954
+ '--project-id',
955
+ '--project-context',
956
+ '--project-namespace',
890
957
  '--reason',
891
958
  '--scope',
892
959
  '--session-id',
@@ -896,6 +963,7 @@ const POSITIONAL_VALUE_FLAGS = new Set([
896
963
  '--token',
897
964
  '--upload-target',
898
965
  '--user-id',
966
+ '--workspace-config',
899
967
  '--workspace-component',
900
968
  ]);
901
969
  function componentPathArg(argv) {
@@ -934,9 +1002,13 @@ async function showStatus(argv) {
934
1002
  };
935
1003
  if (hasFlag(argv, '--json')) {
936
1004
  printJson(output);
937
- return;
938
1005
  }
939
- printBuildSummary(output);
1006
+ else {
1007
+ printBuildSummary(output);
1008
+ }
1009
+ if (hasFlag(argv, '--fail-on-build-failed')) {
1010
+ failIfBuildFailed(output);
1011
+ }
940
1012
  }
941
1013
  async function reindexEvidence(argv) {
942
1014
  const buildId = requiredArg(argv[0], 'reindex requires a build id', 'reindex.build_id.missing');
@@ -1158,6 +1230,54 @@ async function discovery(argv) {
1158
1230
  if (uploadTargets.length > 0)
1159
1231
  console.log(`Upload targets: ${uploadTargets.join(', ')}`);
1160
1232
  }
1233
+ async function initProjectContext(argv) {
1234
+ const root = resolve(componentPathArg(argv) ?? '.');
1235
+ const contextPath = join(root, PROMPTFRAME_PROJECT_CONTEXT_FILE_NAME);
1236
+ if (existsSync(contextPath) && !hasFlag(argv, '--force')) {
1237
+ fail(`PromptFrame project context already exists at ${contextPath}. Use --force to overwrite.`, 'project_context.exists', 2);
1238
+ }
1239
+ const endpoint = resolveEndpoint('init', argv);
1240
+ const payload = await fetchProjects(endpoint, argv);
1241
+ const projects = arrayValue(payload.projects).map((item) => asRecord(item)).filter((item) => Boolean(item));
1242
+ const requestedProjectId = valueAfter(argv, '--project-id');
1243
+ const currentProjectId = requestedProjectId ?? stringValue(payload.currentProjectId);
1244
+ const project = projects.find((item) => stringValue(item.projectId) === currentProjectId || (!requestedProjectId && item.isCurrent === true));
1245
+ if (!project) {
1246
+ fail(requestedProjectId
1247
+ ? `PromptFrame project ${requestedProjectId} is not available for this credential.`
1248
+ : 'PromptFrame did not report an active project for this credential.', 'project_context.project_missing', 2);
1249
+ }
1250
+ const projectId = requiredArg(stringValue(project.projectId), 'PromptFrame project response did not include projectId.', 'project_context.project_id_missing');
1251
+ const tenantId = stringValue(project.tenantId);
1252
+ const context = buildProjectContext({
1253
+ endpoint,
1254
+ tenantId,
1255
+ projectId,
1256
+ projectNamespace: valueAfter(argv, '--project-namespace')
1257
+ ?? stringValue(project.projectNamespace)
1258
+ ?? normalizeProjectNamespace(projectId),
1259
+ defaultUploadTarget: valueAfter(argv, '--target')
1260
+ ?? valueAfter(argv, '--upload-target')
1261
+ ?? 'marketplace_authoring',
1262
+ workspaceConfig: valueAfter(argv, '--workspace-config') ?? 'promptframe-workspace.json',
1263
+ });
1264
+ mkdirSync(root, { recursive: true });
1265
+ writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, 'utf8');
1266
+ const output = {
1267
+ command: 'init',
1268
+ endpoint,
1269
+ contextPath,
1270
+ context,
1271
+ project,
1272
+ diagnostic: diagnostic('project_context.init.completed', 'info', 'PromptFrame project context written.'),
1273
+ };
1274
+ if (hasFlag(argv, '--json')) {
1275
+ printJson(output);
1276
+ return;
1277
+ }
1278
+ console.log(`Wrote PromptFrame project context: ${contextPath}`);
1279
+ console.log(`Project: ${projectId}`);
1280
+ }
1161
1281
  async function project(argv) {
1162
1282
  const subcommand = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'list';
1163
1283
  if (subcommand !== 'list' && subcommand !== 'current') {
@@ -1214,6 +1334,84 @@ async function fetchProjects(endpoint, argv) {
1214
1334
  headers: buildRemoteHeaders(endpoint, argv),
1215
1335
  }, 'project.http.failed');
1216
1336
  }
1337
+ async function component(argv) {
1338
+ const subcommand = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'list';
1339
+ const rest = argv.slice(argv[0] && !argv[0].startsWith('--') ? 1 : 0);
1340
+ switch (subcommand) {
1341
+ case 'list':
1342
+ await componentList(rest);
1343
+ break;
1344
+ case 'create':
1345
+ await componentCreate(rest);
1346
+ break;
1347
+ default:
1348
+ fail(`Unknown component command: ${subcommand}`, 'component.command.unknown', 2);
1349
+ }
1350
+ }
1351
+ async function componentList(argv) {
1352
+ const endpoint = resolveEndpoint('component', argv);
1353
+ const params = new URLSearchParams();
1354
+ const componentId = valueAfter(argv, '--component-id') ?? positionalArgs(argv)[0];
1355
+ if (componentId)
1356
+ params.set('componentId', assertComponentId(componentId));
1357
+ const query = params.toString();
1358
+ const payload = await fetchJson(`${endpoint}/components/marketplace/self/components${query ? `?${query}` : ''}`, {
1359
+ headers: buildRemoteHeaders(endpoint, argv),
1360
+ }, 'component.list.http.failed');
1361
+ const output = {
1362
+ ...payload,
1363
+ command: 'component.list',
1364
+ endpoint,
1365
+ diagnostic: diagnostic('component.list.completed', 'info', 'PromptFrame project components listed.'),
1366
+ };
1367
+ if (hasFlag(argv, '--json')) {
1368
+ printJson(output);
1369
+ return;
1370
+ }
1371
+ for (const item of arrayValue(payload.declarations)) {
1372
+ const declaration = asRecord(item);
1373
+ if (declaration)
1374
+ printComponentDeclarationLine(declaration);
1375
+ }
1376
+ }
1377
+ async function componentCreate(argv) {
1378
+ const endpoint = resolveEndpoint('component', argv);
1379
+ const componentId = assertComponentId(requiredArg(positionalArgs(argv)[0], 'component create requires <component-id>.', 'component.id_missing'));
1380
+ const displayName = requiredArg(valueAfter(argv, '--display-name') ?? valueAfter(argv, '--name') ?? displayNameFromComponentId(componentId), 'component create requires --display-name <name>.', 'component.display_name_missing');
1381
+ const description = valueAfter(argv, '--description');
1382
+ const payload = compactRecord({
1383
+ componentId,
1384
+ displayName,
1385
+ description,
1386
+ });
1387
+ const response = await fetchJson(`${endpoint}/components/marketplace/self/components`, {
1388
+ method: 'POST',
1389
+ headers: {
1390
+ ...buildRemoteHeaders(endpoint, argv),
1391
+ 'content-type': 'application/json',
1392
+ },
1393
+ body: JSON.stringify(payload),
1394
+ }, 'component.create.http.failed');
1395
+ const output = {
1396
+ ...response,
1397
+ command: 'component.create',
1398
+ endpoint,
1399
+ diagnostic: diagnostic('component.create.completed', 'info', 'PromptFrame project component declared.'),
1400
+ };
1401
+ if (hasFlag(argv, '--json')) {
1402
+ printJson(output);
1403
+ return;
1404
+ }
1405
+ const declaration = asRecord(response.declaration);
1406
+ if (declaration)
1407
+ printComponentDeclarationLine(declaration);
1408
+ }
1409
+ function printComponentDeclarationLine(declaration) {
1410
+ const componentId = stringValue(declaration.componentId) ?? 'unknown';
1411
+ const status = stringValue(declaration.status) ?? 'unknown';
1412
+ const displayName = stringValue(declaration.displayName) ?? componentId;
1413
+ console.log(`${componentId}\t${status}\t${displayName}`);
1414
+ }
1217
1415
  function printProjectLine(project, current) {
1218
1416
  const prefix = current ? '*' : '-';
1219
1417
  const projectId = stringValue(project.projectId) ?? 'unknown';
@@ -1428,6 +1626,7 @@ function setupCi(argv) {
1428
1626
  workflowPath,
1429
1627
  requiredSecrets: ['PROMPTFRAME_CI_TOKEN'],
1430
1628
  requiredVariables: ['PROMPTFRAME_API_BASE'],
1629
+ ...(workspaceMode ? { optionalVariables: ['RUNNER_LABELS'] } : {}),
1431
1630
  pullRequestMode: 'check_only',
1432
1631
  pushMode: 'upload',
1433
1632
  diagnostic: diagnostic('setup_ci.github.completed', 'info', 'PromptFrame GitHub Actions workflow written.'),
@@ -1451,18 +1650,20 @@ function setupCiTargetDir(argv) {
1451
1650
  }
1452
1651
  return '.';
1453
1652
  }
1454
- function promptFrameGithubWorkspaceWorkflow(components) {
1653
+ function promptFrameGithubWorkspaceWorkflow(_components) {
1455
1654
  const apiBaseVariable = '$' + '{{ vars.PROMPTFRAME_API_BASE }}';
1456
1655
  const ciTokenSecret = '$' + '{{ secrets.PROMPTFRAME_CI_TOKEN }}';
1457
1656
  const matrixComponentId = '$' + '{{ matrix.component.componentId }}';
1458
1657
  const matrixComponentPath = '$' + '{{ matrix.component.componentPath }}';
1459
1658
  const matrixArtifactName = '$' + '{{ matrix.component.artifactName }}';
1460
- const matrixItems = components.map((component) => [
1461
- ' - componentId: ' + JSON.stringify(component.id),
1462
- ' componentPath: ' + JSON.stringify(component.path),
1463
- ' artifactName: ' + JSON.stringify(githubMatrixArtifactName(component.id)),
1464
- ].join('\n')).join('\n');
1465
- return `name: PromptFrame Component Workspace
1659
+ const runnerLabels = '$' + '{{ vars.RUNNER_LABELS && fromJSON(vars.RUNNER_LABELS) || fromJSON(\'["ubuntu-latest"]\') }}';
1660
+ const workspaceMatrixOutput = '$' + '{{ steps.workspace.outputs.matrix }}';
1661
+ const workspaceCountOutput = '$' + '{{ steps.workspace.outputs.count }}';
1662
+ const discoverMatrix = '$' + '{{ fromJSON(needs.discover.outputs.matrix) }}';
1663
+ return `# promptframe-workflow-version: 1
1664
+ # Generated by promptframe setup-ci --workspace. Keep this header when editing manually.
1665
+
1666
+ name: PromptFrame Component Workspace
1466
1667
 
1467
1668
  on:
1468
1669
  pull_request:
@@ -1477,36 +1678,98 @@ permissions:
1477
1678
  contents: read
1478
1679
 
1479
1680
  jobs:
1681
+ discover:
1682
+ name: Discover PromptFrame components
1683
+ runs-on: ${runnerLabels}
1684
+ outputs:
1685
+ matrix: ${workspaceMatrixOutput}
1686
+ count: ${workspaceCountOutput}
1687
+ steps:
1688
+ - uses: actions/checkout@v4
1689
+ - uses: actions/setup-node@v4
1690
+ with:
1691
+ node-version: '20'
1692
+ - id: workspace
1693
+ name: Read promptframe-workspace.json
1694
+ run: |
1695
+ node <<'NODE'
1696
+ const fs = require('node:fs');
1697
+ const path = require('node:path');
1698
+ const configPath = 'promptframe-workspace.json';
1699
+ function fail(message) {
1700
+ console.error(message);
1701
+ process.exit(1);
1702
+ }
1703
+ function artifactName(value) {
1704
+ return String(value || 'component')
1705
+ .replace(/^@/, '')
1706
+ .replace(/[^A-Za-z0-9_.-]+/g, '-')
1707
+ .replace(/^-+|-+$/g, '') || 'component';
1708
+ }
1709
+ if (!fs.existsSync(configPath)) {
1710
+ fail('promptframe-workspace.json is required for promptframe workspace CI.');
1711
+ }
1712
+ const workspace = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1713
+ const components = Array.isArray(workspace.components) ? workspace.components : [];
1714
+ const include = components.map((component, index) => {
1715
+ if (!component || typeof component.id !== 'string' || typeof component.path !== 'string') {
1716
+ fail('promptframe-workspace.json components[' + index + '] must include string id and path.');
1717
+ }
1718
+ const manifestPath = path.join(component.path, 'manifest.json');
1719
+ if (!fs.existsSync(manifestPath)) {
1720
+ fail('Workspace component manifest not found: ' + manifestPath);
1721
+ }
1722
+ return {
1723
+ componentId: component.id,
1724
+ componentPath: component.path,
1725
+ artifactName: artifactName(component.id),
1726
+ };
1727
+ });
1728
+ const output = process.env.GITHUB_OUTPUT;
1729
+ if (!output) fail('GITHUB_OUTPUT is not available.');
1730
+ fs.appendFileSync(output, 'matrix=' + JSON.stringify({ include }) + '\\n');
1731
+ fs.appendFileSync(output, 'count=' + String(include.length) + '\\n');
1732
+ const summary = process.env.GITHUB_STEP_SUMMARY;
1733
+ if (summary) {
1734
+ const rows = include.map((item) => '- \`' + item.componentId + '\` -> \`' + item.componentPath + '\`');
1735
+ fs.appendFileSync(summary, ['### PromptFrame workspace discover', '', '- Components: \`' + include.length + '\`', ...rows, ''].join('\\n'));
1736
+ }
1737
+ NODE
1738
+
1480
1739
  check:
1481
1740
  name: PromptFrame check (${matrixComponentId})
1482
- runs-on: ubuntu-latest
1741
+ needs: discover
1742
+ if: needs.discover.outputs.count != '0'
1743
+ runs-on: ${runnerLabels}
1483
1744
  strategy:
1484
1745
  fail-fast: false
1485
- matrix:
1486
- component:
1487
- ${matrixItems}
1746
+ matrix: ${discoverMatrix}
1488
1747
  steps:
1489
1748
  - uses: actions/checkout@v4
1490
1749
  - uses: actions/setup-node@v4
1491
1750
  with:
1492
1751
  node-version: '20'
1493
- cache: npm
1494
- - run: npm install
1752
+ cache: pnpm
1753
+ - run: npm install -g pnpm@10
1754
+ - run: pnpm install --no-frozen-lockfile
1495
1755
  - name: Validate workspace
1496
- run: npx promptframe workspace validate . --json | tee promptframe-workspace-report.json
1756
+ env:
1757
+ ARTIFACT_NAME: ${matrixArtifactName}
1758
+ run: npx promptframe workspace validate . --json | tee "promptframe-workspace-report-$ARTIFACT_NAME.json"
1497
1759
  - name: Validate component package
1498
1760
  env:
1499
1761
  PROMPTFRAME_API_BASE: ${apiBaseVariable}
1500
1762
  COMPONENT_ID: ${matrixComponentId}
1501
1763
  COMPONENT_PATH: ${matrixComponentPath}
1764
+ ARTIFACT_NAME: ${matrixArtifactName}
1502
1765
  run: |
1503
1766
  set +e
1504
- npx promptframe check . --workspace-component "$COMPONENT_ID" --json > promptframe-check.json
1767
+ npx promptframe check . --workspace-component "$COMPONENT_ID" --json > "promptframe-check-$ARTIFACT_NAME.json"
1505
1768
  CHECK_EXIT=$?
1506
1769
  set -e
1507
1770
  node <<'NODE'
1508
1771
  const fs = require('node:fs');
1509
- const payload = JSON.parse(fs.readFileSync('promptframe-check.json', 'utf8'));
1772
+ const payload = JSON.parse(fs.readFileSync('promptframe-check-' + process.env.ARTIFACT_NAME + '.json', 'utf8'));
1510
1773
  const diagnostics = Array.isArray(payload.diagnostics) ? payload.diagnostics : [];
1511
1774
  for (const item of diagnostics) {
1512
1775
  const code = String(item.code || 'promptframe.diagnostic');
@@ -1535,40 +1798,40 @@ ${matrixItems}
1535
1798
  - uses: actions/upload-artifact@v4
1536
1799
  if: always()
1537
1800
  with:
1538
- name: promptframe-check-report-${matrixArtifactName}
1801
+ name: promptframe-check-${matrixArtifactName}
1539
1802
  path: |
1540
- promptframe-workspace-report.json
1541
- promptframe-check.json
1803
+ promptframe-workspace-report-${matrixArtifactName}.json
1804
+ promptframe-check-${matrixArtifactName}.json
1542
1805
 
1543
1806
  upload:
1544
1807
  name: PromptFrame upload (${matrixComponentId})
1545
- if: github.event_name == 'push'
1546
- needs: check
1547
- runs-on: ubuntu-latest
1808
+ if: github.event_name == 'push' && needs.discover.outputs.count != '0'
1809
+ needs: [discover, check]
1810
+ runs-on: ${runnerLabels}
1548
1811
  strategy:
1549
1812
  fail-fast: false
1550
- matrix:
1551
- component:
1552
- ${matrixItems}
1813
+ matrix: ${discoverMatrix}
1553
1814
  steps:
1554
1815
  - uses: actions/checkout@v4
1555
1816
  - uses: actions/setup-node@v4
1556
1817
  with:
1557
1818
  node-version: '20'
1558
- cache: npm
1559
- - run: npm install
1819
+ cache: pnpm
1820
+ - run: npm install -g pnpm@10
1821
+ - run: pnpm install --no-frozen-lockfile
1560
1822
  - name: Upload component to PromptFrame
1561
1823
  env:
1562
1824
  PROMPTFRAME_API_BASE: ${apiBaseVariable}
1563
1825
  PROMPTFRAME_CI_TOKEN: ${ciTokenSecret}
1564
1826
  COMPONENT_ID: ${matrixComponentId}
1565
1827
  COMPONENT_PATH: ${matrixComponentPath}
1828
+ ARTIFACT_NAME: ${matrixArtifactName}
1566
1829
  run: |
1567
1830
  set -euo pipefail
1568
- npx promptframe upload . --workspace-component "$COMPONENT_ID" --endpoint "$PROMPTFRAME_API_BASE" --json | tee promptframe-upload.json
1831
+ npx promptframe upload . --workspace-component "$COMPONENT_ID" --endpoint "$PROMPTFRAME_API_BASE" --json | tee "promptframe-upload-$ARTIFACT_NAME.json"
1569
1832
  BUILD_ID=$(node <<'NODE'
1570
1833
  const fs = require('node:fs');
1571
- const payload = JSON.parse(fs.readFileSync('promptframe-upload.json', 'utf8'));
1834
+ const payload = JSON.parse(fs.readFileSync('promptframe-upload-' + process.env.ARTIFACT_NAME + '.json', 'utf8'));
1572
1835
  process.stdout.write(String(payload.jobId || payload.buildId || payload.build?.buildId || ''));
1573
1836
  NODE
1574
1837
  )
@@ -1576,11 +1839,28 @@ ${matrixItems}
1576
1839
  echo "PromptFrame upload response did not include a build id." >&2
1577
1840
  exit 1
1578
1841
  fi
1579
- npx promptframe status "$BUILD_ID" --endpoint "$PROMPTFRAME_API_BASE" --json | tee promptframe-status.json
1842
+ set +e
1843
+ npx promptframe status "$BUILD_ID" --endpoint "$PROMPTFRAME_API_BASE" --json --fail-on-build-failed | tee "promptframe-status-$ARTIFACT_NAME.json"
1844
+ STATUS_EXIT=\${PIPESTATUS[0]}
1845
+ set -e
1580
1846
  node <<'NODE'
1581
1847
  const fs = require('node:fs');
1582
- const payload = JSON.parse(fs.readFileSync('promptframe-upload.json', 'utf8'));
1583
- const status = JSON.parse(fs.readFileSync('promptframe-status.json', 'utf8'));
1848
+ const payload = JSON.parse(fs.readFileSync('promptframe-upload-' + process.env.ARTIFACT_NAME + '.json', 'utf8'));
1849
+ const status = JSON.parse(fs.readFileSync('promptframe-status-' + process.env.ARTIFACT_NAME + '.json', 'utf8'));
1850
+ const build = status.build || {};
1851
+ const buildId = payload.jobId || payload.buildId || build.buildId || 'unknown';
1852
+ const buildStatus = String(build.status || status.status || payload.status || 'queued');
1853
+ const diagnostics = Array.isArray(build.diagnostics) ? build.diagnostics : [];
1854
+ const firstError = diagnostics.find((item) => item && item.severity === 'error') || diagnostics[0] || {};
1855
+ if (['failed', 'cancelled', 'canceled'].includes(buildStatus.toLowerCase())) {
1856
+ const message = [
1857
+ \`buildId=\${buildId}\`,
1858
+ \`status=\${buildStatus}\`,
1859
+ firstError.code ? \`code=\${firstError.code}\` : '',
1860
+ firstError.message ? \`message=\${String(firstError.message).replace(/\\r?\\n/g, ' ').slice(0, 300)}\` : '',
1861
+ ].filter(Boolean).join(' ');
1862
+ console.log(\`::error title=PromptFrame platform build failed::\${message}\`);
1863
+ }
1584
1864
  const summary = process.env.GITHUB_STEP_SUMMARY;
1585
1865
  if (summary) {
1586
1866
  fs.appendFileSync(summary, [
@@ -1589,28 +1869,24 @@ ${matrixItems}
1589
1869
  \`- Component ID: \\\`\${process.env.COMPONENT_ID || payload.source?.workspaceComponentId || 'unknown'}\\\`\`,
1590
1870
  \`- Component path: \\\`\${process.env.COMPONENT_PATH || payload.source?.componentPath || 'unknown'}\\\`\`,
1591
1871
  \`- Diagnostic: \\\`\${payload.diagnostic?.code || 'unknown'}\\\`\`,
1592
- \`- Build ID: \\\`\${payload.jobId || payload.buildId || 'unknown'}\\\`\`,
1593
- \`- Admission status: \\\`\${status.build?.status || status.status || payload.status || 'queued'}\\\`\`,
1872
+ \`- Build ID: \\\`\${buildId}\\\`\`,
1873
+ \`- Platform build status: \\\`\${buildStatus}\\\`\`,
1874
+ firstError.code ? \`- Top diagnostic: \\\`\${firstError.code}\\\`\` : '- Top diagnostic: none',
1875
+ firstError.message ? \`- Message: \${String(firstError.message).replace(/\\r?\\n/g, ' ').slice(0, 300)}\` : '',
1594
1876
  '',
1595
- ].join('\\n'));
1877
+ ].filter(Boolean).join('\\n'));
1596
1878
  }
1597
1879
  NODE
1880
+ exit "$STATUS_EXIT"
1598
1881
  - uses: actions/upload-artifact@v4
1599
1882
  if: always()
1600
1883
  with:
1601
- name: promptframe-upload-report-${matrixArtifactName}
1884
+ name: promptframe-upload-${matrixArtifactName}
1602
1885
  path: |
1603
- promptframe-upload.json
1604
- promptframe-status.json
1886
+ promptframe-upload-${matrixArtifactName}.json
1887
+ promptframe-status-${matrixArtifactName}.json
1605
1888
  `;
1606
1889
  }
1607
- function githubMatrixArtifactName(componentId) {
1608
- return componentId
1609
- .replace(/^@/, '')
1610
- .replace(/[^A-Za-z0-9_.-]+/g, '-')
1611
- .replace(/^-+|-+$/g, '')
1612
- || 'component';
1613
- }
1614
1890
  function promptFrameGithubWorkflow() {
1615
1891
  const apiBaseVariable = '$' + '{{ vars.PROMPTFRAME_API_BASE }}';
1616
1892
  const ciTokenSecret = '$' + '{{ secrets.PROMPTFRAME_CI_TOKEN }}';
@@ -1707,23 +1983,43 @@ jobs:
1707
1983
  echo "PromptFrame upload response did not include a build id." >&2
1708
1984
  exit 1
1709
1985
  fi
1710
- npx promptframe status "$BUILD_ID" --endpoint "$PROMPTFRAME_API_BASE" --json | tee promptframe-status.json
1986
+ set +e
1987
+ npx promptframe status "$BUILD_ID" --endpoint "$PROMPTFRAME_API_BASE" --json --fail-on-build-failed | tee promptframe-status.json
1988
+ STATUS_EXIT=\${PIPESTATUS[0]}
1989
+ set -e
1711
1990
  node <<'NODE'
1712
1991
  const fs = require('node:fs');
1713
1992
  const payload = JSON.parse(fs.readFileSync('promptframe-upload.json', 'utf8'));
1714
1993
  const status = JSON.parse(fs.readFileSync('promptframe-status.json', 'utf8'));
1994
+ const build = status.build || {};
1995
+ const buildId = payload.jobId || payload.buildId || build.buildId || 'unknown';
1996
+ const buildStatus = String(build.status || status.status || payload.status || 'queued');
1997
+ const diagnostics = Array.isArray(build.diagnostics) ? build.diagnostics : [];
1998
+ const firstError = diagnostics.find((item) => item && item.severity === 'error') || diagnostics[0] || {};
1999
+ if (['failed', 'cancelled', 'canceled'].includes(buildStatus.toLowerCase())) {
2000
+ const message = [
2001
+ \`buildId=\${buildId}\`,
2002
+ \`status=\${buildStatus}\`,
2003
+ firstError.code ? \`code=\${firstError.code}\` : '',
2004
+ firstError.message ? \`message=\${String(firstError.message).replace(/\\r?\\n/g, ' ').slice(0, 300)}\` : '',
2005
+ ].filter(Boolean).join(' ');
2006
+ console.log(\`::error title=PromptFrame platform build failed::\${message}\`);
2007
+ }
1715
2008
  const summary = process.env.GITHUB_STEP_SUMMARY;
1716
2009
  if (summary) {
1717
2010
  fs.appendFileSync(summary, [
1718
2011
  '### PromptFrame upload',
1719
2012
  '',
1720
2013
  \`- Diagnostic: \\\`\${payload.diagnostic?.code || 'unknown'}\\\`\`,
1721
- \`- Build ID: \\\`\${payload.jobId || payload.buildId || 'unknown'}\\\`\`,
1722
- \`- Admission status: \\\`\${status.build?.status || status.status || payload.status || 'queued'}\\\`\`,
2014
+ \`- Build ID: \\\`\${buildId}\\\`\`,
2015
+ \`- Platform build status: \\\`\${buildStatus}\\\`\`,
2016
+ firstError.code ? \`- Top diagnostic: \\\`\${firstError.code}\\\`\` : '- Top diagnostic: none',
2017
+ firstError.message ? \`- Message: \${String(firstError.message).replace(/\\r?\\n/g, ' ').slice(0, 300)}\` : '',
1723
2018
  '',
1724
- ].join('\\n'));
2019
+ ].filter(Boolean).join('\\n'));
1725
2020
  }
1726
2021
  NODE
2022
+ exit "$STATUS_EXIT"
1727
2023
  - uses: actions/upload-artifact@v4
1728
2024
  if: always()
1729
2025
  with:
@@ -1735,21 +2031,29 @@ jobs:
1735
2031
  }
1736
2032
  function resolveEndpoint(commandName, argv) {
1737
2033
  const config = readConfig(argv);
2034
+ const credential = asRecord(config.credential);
2035
+ const projectContext = readProjectContext(argv);
1738
2036
  const endpoint = valueAfter(argv, '--endpoint')
1739
2037
  ?? process.env.PROMPTFRAME_API_BASE
1740
2038
  ?? process.env.REMOTION_MEDIA_API_BASE
1741
- ?? stringValue(config.endpoint);
2039
+ ?? stringValue(config.endpoint)
2040
+ ?? stringValue(credential?.endpoint)
2041
+ ?? projectContext?.endpoint;
1742
2042
  if (!endpoint) {
1743
- fail(`${commandName} requires --endpoint, PROMPTFRAME_API_BASE, REMOTION_MEDIA_API_BASE, or local config. No default production endpoint is embedded in the public CLI.`, `${commandName}.endpoint.missing`, 2);
2043
+ fail(`${commandName} requires --endpoint, PROMPTFRAME_API_BASE, REMOTION_MEDIA_API_BASE, local config, or .promptframerc. No default production endpoint is embedded in the public CLI.`, `${commandName}.endpoint.missing`, 2);
1744
2044
  }
1745
2045
  return normalizeEndpoint(endpoint);
1746
2046
  }
1747
2047
  function resolveOptionalEndpoint(argv) {
1748
2048
  const config = readConfig(argv);
2049
+ const credential = asRecord(config.credential);
2050
+ const projectContext = readProjectContext(argv);
1749
2051
  const endpoint = valueAfter(argv, '--endpoint')
1750
2052
  ?? stringValue(process.env.PROMPTFRAME_API_BASE)
1751
2053
  ?? stringValue(process.env.REMOTION_MEDIA_API_BASE)
1752
- ?? stringValue(config.endpoint);
2054
+ ?? stringValue(config.endpoint)
2055
+ ?? stringValue(credential?.endpoint)
2056
+ ?? projectContext?.endpoint;
1753
2057
  return endpoint ? normalizeEndpoint(endpoint) : undefined;
1754
2058
  }
1755
2059
  function buildContextHeaders(argv) {
@@ -1778,7 +2082,7 @@ function buildRemoteHeaders(endpoint, argv, options = {}) {
1778
2082
  'x-auth-permissions',
1779
2083
  ].includes(name));
1780
2084
  if (isFormalEndpoint(endpoint) && hasDevHeaders) {
1781
- fail('Formal PromptFrame endpoints do not accept dev identity headers. Use promptframe login or a scoped CI token.', 'cli.auth.dev_header_formal_endpoint_forbidden');
2085
+ fail(`Formal PromptFrame endpoints do not accept dev identity headers. Run promptframe login --endpoint ${endpoint}, or set PROMPTFRAME_CI_TOKEN to a scoped CI token and retry without --auth-roles/--auth-permissions.`, 'cli.auth.dev_header_formal_endpoint_forbidden');
1782
2086
  }
1783
2087
  if (isFormalEndpoint(endpoint) && options.requireAuth !== false) {
1784
2088
  fail('No PromptFrame CLI credential found for this endpoint. Run promptframe login --endpoint <url> or provide PROMPTFRAME_CI_TOKEN.', 'cli.auth.login_required', 2);
@@ -1918,6 +2222,110 @@ function writeConfig(argv, config) {
1918
2222
  function resolveConfigPath(argv) {
1919
2223
  return resolve(valueAfter(argv, '--config') ?? process.env.PROMPTFRAME_CONFIG ?? join(os.homedir(), '.promptframe', 'component-authoring.json'));
1920
2224
  }
2225
+ function readProjectContext(argv) {
2226
+ const explicitPath = valueAfter(argv, '--project-context');
2227
+ const contextPath = explicitPath
2228
+ ? resolve(explicitPath)
2229
+ : findProjectContextPath(process.cwd());
2230
+ if (!contextPath || !existsSync(contextPath))
2231
+ return undefined;
2232
+ let raw;
2233
+ try {
2234
+ raw = JSON.parse(readFileSync(contextPath, 'utf8'));
2235
+ }
2236
+ catch {
2237
+ fail(`PromptFrame project context is not valid JSON: ${contextPath}`, 'project_context.invalid_json', 2);
2238
+ }
2239
+ return parseProjectContext(raw, contextPath);
2240
+ }
2241
+ function findProjectContextPath(startDir) {
2242
+ let current = resolve(startDir);
2243
+ for (;;) {
2244
+ const candidate = join(current, PROMPTFRAME_PROJECT_CONTEXT_FILE_NAME);
2245
+ if (existsSync(candidate))
2246
+ return candidate;
2247
+ const parent = dirname(current);
2248
+ if (parent === current)
2249
+ return undefined;
2250
+ current = parent;
2251
+ }
2252
+ }
2253
+ function parseProjectContext(value, sourcePath) {
2254
+ const record = asRecord(value);
2255
+ if (!record) {
2256
+ fail(`PromptFrame project context must be a JSON object: ${sourcePath}`, 'project_context.invalid', 2);
2257
+ }
2258
+ assertProjectContextSecretFree(record, sourcePath);
2259
+ const schemaVersion = stringValue(record.schemaVersion);
2260
+ if (schemaVersion && schemaVersion !== PROMPTFRAME_PROJECT_CONTEXT_SCHEMA_VERSION) {
2261
+ fail(`Unsupported PromptFrame project context schemaVersion ${schemaVersion}. Expected ${PROMPTFRAME_PROJECT_CONTEXT_SCHEMA_VERSION}.`, 'project_context.schema_unsupported', 2);
2262
+ }
2263
+ return {
2264
+ schemaVersion: PROMPTFRAME_PROJECT_CONTEXT_SCHEMA_VERSION,
2265
+ ...(stringValue(record.endpoint) ? { endpoint: normalizeEndpoint(stringValue(record.endpoint)) } : {}),
2266
+ ...(stringValue(record.tenantId) ? { tenantId: stringValue(record.tenantId) } : {}),
2267
+ ...(stringValue(record.projectId) ? { projectId: stringValue(record.projectId) } : {}),
2268
+ ...(stringValue(record.projectNamespace) ? { projectNamespace: stringValue(record.projectNamespace) } : {}),
2269
+ ...(stringValue(record.defaultUploadTarget) ? { defaultUploadTarget: stringValue(record.defaultUploadTarget) } : {}),
2270
+ ...(stringValue(record.workspaceConfig) ? { workspaceConfig: stringValue(record.workspaceConfig) } : {}),
2271
+ };
2272
+ }
2273
+ function buildProjectContext(input) {
2274
+ const context = {
2275
+ schemaVersion: PROMPTFRAME_PROJECT_CONTEXT_SCHEMA_VERSION,
2276
+ endpoint: normalizeEndpoint(input.endpoint),
2277
+ ...(input.tenantId ? { tenantId: input.tenantId } : {}),
2278
+ projectId: input.projectId,
2279
+ ...(input.projectNamespace ? { projectNamespace: normalizeProjectNamespace(input.projectNamespace) } : {}),
2280
+ ...(input.defaultUploadTarget ? { defaultUploadTarget: input.defaultUploadTarget } : {}),
2281
+ ...(input.workspaceConfig ? { workspaceConfig: input.workspaceConfig } : {}),
2282
+ };
2283
+ assertProjectContextSecretFree(context, PROMPTFRAME_PROJECT_CONTEXT_FILE_NAME);
2284
+ return context;
2285
+ }
2286
+ function assertProjectContextSecretFree(value, sourcePath) {
2287
+ if (!value || typeof value !== 'object')
2288
+ return;
2289
+ if (Array.isArray(value)) {
2290
+ for (const item of value)
2291
+ assertProjectContextSecretFree(item, sourcePath);
2292
+ return;
2293
+ }
2294
+ for (const [key, child] of Object.entries(value)) {
2295
+ const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, '');
2296
+ if (PROJECT_CONTEXT_FORBIDDEN_KEY_NAMES.has(normalizedKey)) {
2297
+ fail(`PromptFrame project context must not contain secret-bearing field "${key}" in ${sourcePath}. Store tokens in the local CLI config or CI secret store instead.`, 'project_context.secret_field_forbidden', 2);
2298
+ }
2299
+ assertProjectContextSecretFree(child, sourcePath);
2300
+ }
2301
+ }
2302
+ function assertComponentId(value) {
2303
+ if (!/^@[a-z0-9][a-z0-9-]{1,62}\/[a-z0-9][a-z0-9-]{1,62}$/.test(value)) {
2304
+ fail(`Invalid component id "${value}". Expected @project-namespace/component-slug.`, 'component.id_invalid', 2);
2305
+ }
2306
+ return value;
2307
+ }
2308
+ function normalizeProjectNamespace(value) {
2309
+ const normalized = value
2310
+ .trim()
2311
+ .toLowerCase()
2312
+ .replace(/^@/, '')
2313
+ .replace(/[^a-z0-9-]+/g, '-')
2314
+ .replace(/^-+|-+$/g, '')
2315
+ .slice(0, 63);
2316
+ if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(normalized)) {
2317
+ fail(`Invalid project namespace "${value}".`, 'project_context.namespace_invalid', 2);
2318
+ }
2319
+ return normalized;
2320
+ }
2321
+ function displayNameFromComponentId(componentId) {
2322
+ const slug = componentId.split('/').at(-1) ?? componentId;
2323
+ return slug
2324
+ .split('-')
2325
+ .filter(Boolean)
2326
+ .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)
2327
+ .join(' ');
2328
+ }
1921
2329
  async function fetchJson(url, init, code) {
1922
2330
  const response = await fetch(url, init);
1923
2331
  const payload = await response.json().catch(() => ({}));
@@ -2385,7 +2793,7 @@ function collectPackageFiles(root) {
2385
2793
  return files.sort((a, b) => a.name.localeCompare(b.name));
2386
2794
  }
2387
2795
  function buildLockfileEvidenceFile(root) {
2388
- const lockfiles = collectRootPackageManagerLockfiles(root);
2796
+ const lockfiles = collectPackageManagerLockfileEvidence(root);
2389
2797
  if (lockfiles.length === 0)
2390
2798
  return undefined;
2391
2799
  const packageJson = readFileSync(join(root, 'package.json'));
@@ -2394,6 +2802,8 @@ function buildLockfileEvidenceFile(root) {
2394
2802
  packageJsonSha256: sha256Buffer(packageJson),
2395
2803
  lockfiles: lockfiles.map((lockfile) => ({
2396
2804
  fileName: lockfile.name,
2805
+ source: lockfile.source,
2806
+ relativePath: lockfile.relativePath,
2397
2807
  sizeBytes: lockfile.data.byteLength,
2398
2808
  sha256: sha256Buffer(lockfile.data),
2399
2809
  })),
@@ -2403,16 +2813,47 @@ function buildLockfileEvidenceFile(root) {
2403
2813
  data: Buffer.from(`${JSON.stringify(receipt)}\n`, 'utf8'),
2404
2814
  };
2405
2815
  }
2406
- function collectRootPackageManagerLockfiles(root) {
2816
+ function collectPackageManagerLockfileEvidence(root) {
2817
+ const directLockfiles = collectDirectPackageManagerLockfiles(root);
2818
+ if (directLockfiles.length > 0)
2819
+ return directLockfiles;
2820
+ const workspaceLockfile = findPnpmWorkspaceRootLockfile(root);
2821
+ return workspaceLockfile ? [workspaceLockfile] : [];
2822
+ }
2823
+ function collectDirectPackageManagerLockfiles(root) {
2407
2824
  const lockfiles = [];
2408
2825
  for (const name of PACKAGE_MANAGER_LOCKFILE_NAMES) {
2409
2826
  const full = join(root, name);
2410
2827
  if (!existsSync(full))
2411
2828
  continue;
2412
- lockfiles.push({ name, data: readFileSync(full) });
2829
+ lockfiles.push({
2830
+ name,
2831
+ data: readFileSync(full),
2832
+ source: 'component',
2833
+ relativePath: name,
2834
+ });
2413
2835
  }
2414
2836
  return lockfiles;
2415
2837
  }
2838
+ function findPnpmWorkspaceRootLockfile(root) {
2839
+ let current = resolve(root);
2840
+ while (true) {
2841
+ const workspaceConfig = join(current, 'pnpm-workspace.yaml');
2842
+ const lockfile = join(current, 'pnpm-lock.yaml');
2843
+ if (existsSync(workspaceConfig) && existsSync(lockfile)) {
2844
+ return {
2845
+ name: 'pnpm-lock.yaml',
2846
+ data: readFileSync(lockfile),
2847
+ source: 'workspace_root',
2848
+ relativePath: relative(root, lockfile).replace(/\\/g, '/'),
2849
+ };
2850
+ }
2851
+ const parent = dirname(current);
2852
+ if (parent === current)
2853
+ return undefined;
2854
+ current = parent;
2855
+ }
2856
+ }
2416
2857
  function sha256Buffer(data) {
2417
2858
  return `sha256:${createHash('sha256').update(data).digest('hex')}`;
2418
2859
  }
@@ -2602,6 +3043,26 @@ function printBuildSummary(payload) {
2602
3043
  console.log(`${stringValue(diagnosticItem?.severity)?.toUpperCase() ?? 'INFO'} ${stringValue(diagnosticItem?.code) ?? 'unknown'}: ${stringValue(diagnosticItem?.message) ?? ''}`);
2603
3044
  }
2604
3045
  }
3046
+ function failIfBuildFailed(payload) {
3047
+ const status = getBuildStatus(payload);
3048
+ if (!status || !['failed', 'cancelled', 'canceled'].includes(status.toLowerCase()))
3049
+ return;
3050
+ const buildId = getBuildId(payload) ?? 'unknown';
3051
+ const diagnosticItem = firstBuildDiagnostic(payload);
3052
+ const diagnosticText = diagnosticItem
3053
+ ? `${stringValue(diagnosticItem.code) ?? 'unknown'}: ${stringValue(diagnosticItem.message) ?? ''}`.trim()
3054
+ : 'no diagnostic';
3055
+ fail(`PromptFrame platform build failed: buildId=${buildId} status=${status} diagnostic=${diagnosticText}`, 'status.build.failed', 1);
3056
+ }
3057
+ function getBuildStatus(payload) {
3058
+ return stringValue(asRecord(payload.build)?.status) ?? stringValue(payload.status);
3059
+ }
3060
+ function firstBuildDiagnostic(payload) {
3061
+ const diagnostics = arrayValue(asRecord(payload.build)?.diagnostics)
3062
+ .map((item) => asRecord(item))
3063
+ .filter((item) => Boolean(item));
3064
+ return diagnostics.find((item) => item.severity === 'error') ?? diagnostics[0];
3065
+ }
2605
3066
  function printStatusUrl(endpoint, payload) {
2606
3067
  const statusUrl = stringValue(payload.statusUrl) ?? stringValue(asRecord(payload.build)?.statusUrl);
2607
3068
  if (!statusUrl)
@@ -2650,6 +3111,7 @@ Commands:
2650
3111
  --target <target> marketplace_authoring or project_private_generation
2651
3112
  --target marketplace --strict Alias for strict external marketplace authoring
2652
3113
  status <buildId> Fetch component build status
3114
+ --fail-on-build-failed Exit nonzero when platform build status is failed/cancelled
2653
3115
  reindex <buildId> Rebuild component search/evidence indexes
2654
3116
  probe <buildId> --level <level> Rerun component layout/security probe
2655
3117
  login --endpoint <url> Start browser login code flow and store a CLI token
@@ -2657,13 +3119,15 @@ Commands:
2657
3119
  whoami Show the current PromptFrame CLI identity
2658
3120
  logout Revoke the current CLI token and clear local config
2659
3121
  discovery Fetch platform endpoint and self-service capabilities
3122
+ init [dir] Write secret-free .promptframerc project context
2660
3123
  project list|current List accessible projects or show the active project
3124
+ component list|create List or declare Project-scoped components
2661
3125
  ci-token create|list|revoke Manage self-service CI tokens for the current project
2662
3126
  setup-ci [dir] --provider github Write a GitHub Actions workflow skeleton
2663
3127
  configure --endpoint <url> Write local CLI endpoint/context config
2664
3128
 
2665
3129
  Endpoint resolution:
2666
- --endpoint, PROMPTFRAME_API_BASE, REMOTION_MEDIA_API_BASE, then local config.
3130
+ --endpoint, PROMPTFRAME_API_BASE, REMOTION_MEDIA_API_BASE, local config, then .promptframerc.
2667
3131
  The public CLI embeds no production/private endpoint defaults.
2668
3132
  Auth context:
2669
3133
  promptframe login, PROMPTFRAME_CI_TOKEN, or PROMPTFRAME_CLI_TOKEN for formal endpoints.