@promptframe/cli 0.1.30 → 0.1.32

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
@@ -19,6 +19,21 @@ const DEFAULT_CLI_LOGIN_SCOPES = ['component.upload', 'component.status.read'];
19
19
  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
+ 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
+ ]);
22
37
  const PACKAGE_MANAGER_LOCKFILE_NAMES = [
23
38
  'package-lock.json',
24
39
  'npm-shrinkwrap.json',
@@ -83,16 +98,26 @@ async function run(name, argv) {
83
98
  case 'discovery':
84
99
  await discovery(argv);
85
100
  break;
101
+ case 'init':
102
+ await initProjectContext(argv);
103
+ break;
86
104
  case 'project':
87
105
  await project(argv);
88
106
  break;
89
107
  case 'projects':
90
108
  await project(['list', ...argv]);
91
109
  break;
110
+ case 'component':
111
+ case 'components':
112
+ await component(argv);
113
+ break;
92
114
  case 'ci-token':
93
115
  case 'token':
94
116
  await ciToken(argv);
95
117
  break;
118
+ case 'workspace':
119
+ workspace(argv);
120
+ break;
96
121
  case 'setup-ci':
97
122
  setupCi(argv);
98
123
  break;
@@ -157,7 +182,8 @@ function doctor(argv) {
157
182
  console.log(`doctor passed: ${dir}`);
158
183
  }
159
184
  function validate(argv) {
160
- const dir = resolve(firstPositionalArg(argv) ?? '.');
185
+ const input = resolveComponentInput(argv);
186
+ const dir = input.dir;
161
187
  const manifest = validateComponentDirectory(dir);
162
188
  const publicResources = evaluateDirectoryPublicResources(dir);
163
189
  assertPublicResourcesAccepted(publicResources);
@@ -171,6 +197,7 @@ function validate(argv) {
171
197
  const output = {
172
198
  command: 'validate',
173
199
  dir,
200
+ source: input.source,
174
201
  manifest: {
175
202
  id: manifest.id,
176
203
  name: manifest.name,
@@ -195,7 +222,8 @@ function validate(argv) {
195
222
  printDiagnostics(diagnostics);
196
223
  }
197
224
  async function check(argv) {
198
- const dir = resolve(firstPositionalArg(argv) ?? '.');
225
+ const input = resolveComponentInput(argv);
226
+ const dir = input.dir;
199
227
  const target = resolveUploadTarget(argv);
200
228
  const manifest = validateComponentDirectory(dir);
201
229
  assertAuthoringPackageFreshness(dir, target);
@@ -214,6 +242,7 @@ async function check(argv) {
214
242
  const output = {
215
243
  command: 'check',
216
244
  dir,
245
+ source: input.source,
217
246
  manifest: manifestSummary(manifest),
218
247
  checkedRuleIds: VALIDATE_CHECKED_RULE_IDS,
219
248
  securityPolicyVersion: PROMPTFRAME_PUBLIC_SECURITY_POLICY.policyVersion,
@@ -267,7 +296,8 @@ function upgrade(argv) {
267
296
  }
268
297
  }
269
298
  function preview(argv) {
270
- const dir = resolve(firstPositionalArg(argv) ?? '.');
299
+ const input = resolveComponentInput(argv);
300
+ const dir = input.dir;
271
301
  const manifest = validateComponentDirectory(dir);
272
302
  const previewEnvelope = readPreviewProps(dir);
273
303
  const previewScript = resolveLocalPreviewScript(readPackageManifest(join(dir, 'package.json')));
@@ -282,6 +312,7 @@ function preview(argv) {
282
312
  const output = {
283
313
  command: 'preview',
284
314
  dir,
315
+ source: input.source,
285
316
  manifest: manifestSummary(manifest),
286
317
  renderingSystem: 'remotion',
287
318
  previewSource: 'src/preview-props.json',
@@ -302,7 +333,8 @@ function preview(argv) {
302
333
  console.log(`Run: npm run ${previewScript}`);
303
334
  }
304
335
  async function dev(argv) {
305
- const dir = resolve(firstPositionalArg(argv) ?? '.');
336
+ const input = resolveComponentInput(argv);
337
+ const dir = input.dir;
306
338
  const target = resolveUploadTarget(argv);
307
339
  const manifest = validateComponentDirectory(dir);
308
340
  assertAuthoringPackageFreshness(dir, target);
@@ -315,6 +347,7 @@ async function dev(argv) {
315
347
  const output = {
316
348
  command: 'dev',
317
349
  dir,
350
+ source: input.source,
318
351
  manifest: manifestSummary(manifest),
319
352
  renderingSystem: 'remotion-player',
320
353
  previewSource: 'src/preview-props.json',
@@ -343,13 +376,15 @@ async function dev(argv) {
343
376
  await runDevServer(dir, devCommand);
344
377
  }
345
378
  function packageComponent(argv) {
346
- const artifact = packageDirectory(argv[0] ?? '.', valueAfter(argv, '--out'));
379
+ const input = resolveComponentInput(argv);
380
+ const artifact = packageDirectory(input.dir, valueAfter(argv, '--out'));
347
381
  printJson({
348
382
  command: 'package',
349
383
  diagnostic: diagnostic('package.completed', 'info', 'Component source archive created.'),
350
384
  out: artifact.out,
351
385
  sizeBytes: artifact.sizeBytes,
352
386
  sha256: artifact.sha256,
387
+ source: input.source,
353
388
  publicResources: artifact.publicResources,
354
389
  });
355
390
  }
@@ -444,18 +479,25 @@ async function remoteCommand(name, argv) {
444
479
  }
445
480
  }
446
481
  async function uploadComponent(argv) {
447
- const target = resolve(argv[0] ?? '.');
482
+ const workspaceComponent = valueAfter(argv, '--workspace-component');
483
+ const target = resolve(componentPathArg(argv) ?? '.');
448
484
  const uploadTarget = resolveUploadTarget(argv);
449
485
  let localReusability;
486
+ if (workspaceComponent && target.endsWith('.zip')) {
487
+ fail('upload --workspace-component requires a workspace root, not a zip file.', 'workspace.component.zip_unsupported', 2);
488
+ }
489
+ const input = target.endsWith('.zip')
490
+ ? { dir: target, source: { mode: 'zip' } }
491
+ : resolveComponentInput(argv);
450
492
  const artifact = target.endsWith('.zip')
451
493
  ? packageZipForUpload(target, uploadTarget)
452
- : packageDirectoryForUploadWithLocalReusability(target, uploadTarget, valueAfter(argv, '--out'), (reusability) => {
494
+ : packageDirectoryForUploadWithLocalReusability(input.dir, uploadTarget, valueAfter(argv, '--out'), (reusability) => {
453
495
  localReusability = reusability;
454
496
  });
455
497
  const localDiagnostics = localReusability
456
498
  ? [
457
499
  ...reusabilityDiagnostics(localReusability),
458
- ...stylePropDiagnostics(target),
500
+ ...stylePropDiagnostics(input.dir),
459
501
  ...(artifact.publicResources?.diagnostics ?? []),
460
502
  ]
461
503
  : undefined;
@@ -470,19 +512,22 @@ async function uploadComponent(argv) {
470
512
  headers: {
471
513
  ...buildRemoteHeaders(endpoint, argv),
472
514
  ...buildSecurityPolicyHeaders(),
515
+ ...buildSourceMetadataHeaders(input.source),
473
516
  'x-promptframe-upload-target': uploadTarget,
474
517
  },
475
518
  }, 'upload.http.failed');
476
- const output = {
477
- ...payload,
519
+ const safePayload = sanitizePublicUploadPayload(payload);
520
+ const output = sanitizePublicUploadPayload({
521
+ ...safePayload,
478
522
  command: 'upload',
479
523
  endpoint,
480
524
  uploadTarget,
481
525
  jobId: getBuildId(payload),
482
526
  package: artifact,
527
+ source: input.source,
483
528
  ...(localReusability ? { localReusability, diagnostics: localDiagnostics } : {}),
484
529
  diagnostic: diagnostic('upload.completed', 'info', 'Component upload accepted by platform.'),
485
- };
530
+ });
486
531
  if (hasFlag(argv, '--json')) {
487
532
  printJson(output);
488
533
  return;
@@ -490,10 +535,56 @@ async function uploadComponent(argv) {
490
535
  console.log('Upload accepted by PromptFrame platform.');
491
536
  console.log(`Build: ${output.jobId ?? 'unknown'}`);
492
537
  console.log(`Status: ${stringValue(payload.status) ?? stringValue(asRecord(payload.build)?.status) ?? 'queued'}`);
538
+ if (output.jobId) {
539
+ console.log(`Next: promptframe status ${output.jobId} --endpoint ${endpoint}`);
540
+ }
493
541
  if (localDiagnostics)
494
542
  printDiagnostics(localDiagnostics);
495
543
  printStatusUrl(endpoint, payload);
496
544
  }
545
+ const PUBLIC_UPLOAD_JSON_OMIT_KEYS = new Set([
546
+ 'providerUsageReceipt',
547
+ 'providerUsageReceipts',
548
+ 'vectorRef',
549
+ 'retryKey',
550
+ 'sourceHash',
551
+ 'sourceHashDetails',
552
+ 'internalSourceHash',
553
+ 'schemaHash',
554
+ 'manifestHash',
555
+ 'bundleHash',
556
+ 'evidence',
557
+ 'evidenceRecords',
558
+ 'evidenceItems',
559
+ 'rawEvidence',
560
+ ]);
561
+ function sanitizePublicUploadPayload(value) {
562
+ return asRecord(sanitizePublicUploadValue(value)) ?? {};
563
+ }
564
+ function sanitizePublicUploadValue(value) {
565
+ if (typeof value === 'string') {
566
+ return scrubInternalReferences(value);
567
+ }
568
+ if (Array.isArray(value)) {
569
+ return value.map((item) => sanitizePublicUploadValue(item));
570
+ }
571
+ const record = asRecord(value);
572
+ if (!record)
573
+ return value;
574
+ const output = {};
575
+ for (const [key, entry] of Object.entries(record)) {
576
+ if (PUBLIC_UPLOAD_JSON_OMIT_KEYS.has(key))
577
+ continue;
578
+ output[key] = sanitizePublicUploadValue(entry);
579
+ }
580
+ return output;
581
+ }
582
+ function scrubInternalReferences(value) {
583
+ return value
584
+ .replace(/\bREQ-\d+\b/g, 'internal reference')
585
+ .replace(/\bTASK-\d+(?:-[A-Za-z0-9-]+)?\b/g, 'internal task')
586
+ .replace(/\bBUG-\d+\b/g, 'internal bug');
587
+ }
497
588
  function resolveUploadTarget(argv) {
498
589
  const raw = normalizeUploadTargetAlias(valueAfter(argv, '--target') ?? valueAfter(argv, '--upload-target') ?? 'marketplace_authoring');
499
590
  const parsed = authoringUploadTargetSchema.safeParse(raw);
@@ -573,6 +664,331 @@ function buildFreshnessWarningDecision(target, code, message) {
573
664
  function formatRemoteStandardStaleFailure(remoteSourceHash) {
574
665
  return `PromptFrame component standard is stale: local=${COMPONENT_STANDARD_SOURCE_HASH}, platform=${remoteSourceHash}. Run promptframe upgrade . --apply before upload.`;
575
666
  }
667
+ function workspace(argv) {
668
+ const subcommand = argv[0] ?? 'validate';
669
+ const rest = argv.slice(argv[0] ? 1 : 0);
670
+ switch (subcommand) {
671
+ case 'validate':
672
+ workspaceValidate(rest);
673
+ break;
674
+ case 'list':
675
+ workspaceList(rest);
676
+ break;
677
+ case 'init':
678
+ workspaceInit(rest);
679
+ break;
680
+ case 'add':
681
+ workspaceAdd(rest);
682
+ break;
683
+ default:
684
+ fail(`Unknown workspace command: ${subcommand}`, 'workspace.command.unknown', 2);
685
+ }
686
+ }
687
+ function workspaceValidate(argv) {
688
+ const root = resolve(componentPathArg(argv) ?? '.');
689
+ const reports = collectWorkspaceComponentReports(root);
690
+ const output = {
691
+ command: 'workspace.validate',
692
+ workspace: workspaceSummary(root),
693
+ components: reports.map(workspaceReportForOutput),
694
+ diagnostic: diagnostic('workspace.validate.completed', 'info', 'PromptFrame workspace configuration is valid.'),
695
+ };
696
+ if (hasFlag(argv, '--json')) {
697
+ printJson(output);
698
+ return;
699
+ }
700
+ console.log(`workspace valid: ${root}`);
701
+ for (const component of reports) {
702
+ console.log(`${component.id} -> ${component.path}`);
703
+ }
704
+ }
705
+ function workspaceList(argv) {
706
+ const root = resolve(componentPathArg(argv) ?? '.');
707
+ const reports = collectWorkspaceComponentReports(root);
708
+ const output = {
709
+ command: 'workspace.list',
710
+ workspace: workspaceSummary(root),
711
+ components: reports.map(workspaceReportForOutput),
712
+ diagnostic: diagnostic('workspace.list.completed', 'info', 'PromptFrame workspace components listed.'),
713
+ };
714
+ if (hasFlag(argv, '--json')) {
715
+ printJson(output);
716
+ return;
717
+ }
718
+ for (const component of reports) {
719
+ console.log(`${component.id}\t${component.path}`);
720
+ }
721
+ }
722
+ function workspaceInit(argv) {
723
+ const root = resolve(componentPathArg(argv) ?? '.');
724
+ const configPath = workspaceConfigPath(root);
725
+ if (existsSync(configPath) && !hasFlag(argv, '--force')) {
726
+ fail(`PromptFrame workspace config already exists at ${configPath}. Use --force to overwrite.`, 'workspace.config.exists');
727
+ }
728
+ const componentPath = valueAfter(argv, '--component-path');
729
+ const componentId = valueAfter(argv, '--id');
730
+ const components = componentPath || componentId
731
+ ? [{
732
+ id: componentId ?? fail('--id is required when --component-path is provided.', 'workspace.component.id_missing', 2),
733
+ path: normalizeWorkspacePath(componentPath ?? fail('--component-path is required when --id is provided.', 'workspace.component.path_missing', 2)),
734
+ }]
735
+ : [];
736
+ writeWorkspaceConfig(root, components);
737
+ const output = {
738
+ command: 'workspace.init',
739
+ workspace: workspaceSummary(root),
740
+ components,
741
+ diagnostic: diagnostic('workspace.init.completed', 'info', 'PromptFrame workspace configuration written.'),
742
+ };
743
+ if (hasFlag(argv, '--json')) {
744
+ printJson(output);
745
+ return;
746
+ }
747
+ console.log(`Wrote PromptFrame workspace config: ${configPath}`);
748
+ }
749
+ function workspaceAdd(argv) {
750
+ const positionals = positionalArgs(argv);
751
+ const root = resolve(positionals.length >= 2 ? positionals[0] : '.');
752
+ const rawPath = positionals.length >= 2 ? positionals[1] : positionals[0];
753
+ const componentPath = normalizeWorkspacePath(rawPath ?? fail('workspace add requires a component path.', 'workspace.component.path_missing', 2));
754
+ const id = valueAfter(argv, '--id') ?? fail('workspace add requires --id <component-id>.', 'workspace.component.id_missing', 2);
755
+ const current = readWorkspaceConfig(root);
756
+ const components = [...current.components, { id, path: componentPath }];
757
+ assertNoDuplicateWorkspaceComponents(components);
758
+ writeWorkspaceConfig(root, components);
759
+ const output = {
760
+ command: 'workspace.add',
761
+ workspace: workspaceSummary(root),
762
+ component: { id, path: componentPath },
763
+ diagnostic: diagnostic('workspace.add.completed', 'info', 'PromptFrame workspace component added.'),
764
+ };
765
+ if (hasFlag(argv, '--json')) {
766
+ printJson(output);
767
+ return;
768
+ }
769
+ console.log(`Added workspace component: ${id} -> ${componentPath}`);
770
+ }
771
+ function resolveComponentInput(argv) {
772
+ const workspaceComponentId = valueAfter(argv, '--workspace-component');
773
+ if (!workspaceComponentId) {
774
+ return {
775
+ dir: resolve(componentPathArg(argv) ?? '.'),
776
+ source: {
777
+ mode: 'single_component',
778
+ componentPath: '.',
779
+ },
780
+ };
781
+ }
782
+ const root = resolve(componentPathArg(argv) ?? '.');
783
+ const report = resolveWorkspaceComponent(root, workspaceComponentId);
784
+ return {
785
+ dir: report.absolutePath,
786
+ source: {
787
+ mode: 'workspace',
788
+ workspaceConfig: 'promptframe-workspace.json',
789
+ workspaceComponentId: report.id,
790
+ componentPath: report.path,
791
+ manifestId: stringValue(report.manifest.id) ?? report.id,
792
+ },
793
+ };
794
+ }
795
+ function resolveWorkspaceComponent(root, componentId) {
796
+ const reports = collectWorkspaceComponentReports(root);
797
+ const report = reports.find((component) => component.id === componentId);
798
+ if (!report) {
799
+ fail(`PromptFrame workspace component not found: ${componentId}. Run promptframe workspace list ${root} to inspect configured ids.`, 'workspace.component.not_found', 2);
800
+ }
801
+ return report;
802
+ }
803
+ function collectWorkspaceComponentReports(root) {
804
+ const workspace = readWorkspaceConfig(root);
805
+ if (workspace.components.length === 0) {
806
+ fail('PromptFrame workspace config does not list any components.', 'workspace.components.empty', 2);
807
+ }
808
+ return workspace.components.map((component) => {
809
+ const absolutePath = resolve(root, component.path);
810
+ assertWorkspacePathInsideRoot(root, absolutePath, component.path);
811
+ if (!existsSync(absolutePath)) {
812
+ fail(`PromptFrame workspace component path does not exist: ${component.path}`, 'workspace.component.path_missing', 2);
813
+ }
814
+ const manifest = validateComponentDirectory(absolutePath);
815
+ if (manifest.id !== component.id) {
816
+ fail(`PromptFrame workspace component ${component.id} points to ${component.path}, but manifest.json declares ${manifest.id}. Keep promptframe-workspace.json and manifest.json ids identical.`, 'workspace.component.manifest_id_mismatch');
817
+ }
818
+ return {
819
+ ...component,
820
+ absolutePath,
821
+ manifest: manifestSummary(manifest),
822
+ };
823
+ });
824
+ }
825
+ function workspaceReportForOutput(report) {
826
+ return {
827
+ id: report.id,
828
+ path: report.path,
829
+ manifest: report.manifest,
830
+ };
831
+ }
832
+ function workspaceSummary(root) {
833
+ return {
834
+ schemaVersion: PROMPTFRAME_WORKSPACE_SCHEMA_VERSION,
835
+ root,
836
+ configPath: workspaceConfigPath(root),
837
+ };
838
+ }
839
+ function workspaceConfigPath(root) {
840
+ return join(root, 'promptframe-workspace.json');
841
+ }
842
+ function readWorkspaceConfig(root) {
843
+ const configPath = workspaceConfigPath(root);
844
+ if (!existsSync(configPath)) {
845
+ fail(`PromptFrame workspace config not found at ${configPath}.`, 'workspace.config.missing', 2);
846
+ }
847
+ let raw;
848
+ try {
849
+ raw = JSON.parse(readFileSync(configPath, 'utf8'));
850
+ }
851
+ catch {
852
+ fail(`PromptFrame workspace config must be valid JSON: ${configPath}`, 'workspace.config.invalid_json', 2);
853
+ }
854
+ const config = asRecord(raw);
855
+ if (!config) {
856
+ fail('PromptFrame workspace config must be a JSON object.', 'workspace.config.invalid', 2);
857
+ }
858
+ const schemaVersion = stringValue(config.schemaVersion);
859
+ if (schemaVersion !== PROMPTFRAME_WORKSPACE_SCHEMA_VERSION) {
860
+ fail(`Unsupported PromptFrame workspace schemaVersion: ${schemaVersion ?? '<missing>'}. Expected ${PROMPTFRAME_WORKSPACE_SCHEMA_VERSION}.`, 'workspace.config.schema_version_unsupported', 2);
861
+ }
862
+ const rawComponents = arrayValue(config.components);
863
+ const components = rawComponents.map((item, index) => {
864
+ const component = asRecord(item);
865
+ if (!component) {
866
+ fail(`PromptFrame workspace components[${index}] must be a JSON object.`, 'workspace.component.invalid', 2);
867
+ }
868
+ const id = stringValue(component.id);
869
+ const path = stringValue(component.path);
870
+ if (!id) {
871
+ fail(`PromptFrame workspace components[${index}].id is required.`, 'workspace.component.id_missing', 2);
872
+ }
873
+ if (!path) {
874
+ fail(`PromptFrame workspace components[${index}].path is required.`, 'workspace.component.path_missing', 2);
875
+ }
876
+ return {
877
+ id,
878
+ path: normalizeWorkspacePath(path),
879
+ };
880
+ });
881
+ assertNoDuplicateWorkspaceComponents(components);
882
+ return { schemaVersion, components };
883
+ }
884
+ function writeWorkspaceConfig(root, components) {
885
+ assertNoDuplicateWorkspaceComponents(components);
886
+ mkdirSync(root, { recursive: true });
887
+ writeFileSync(workspaceConfigPath(root), `${JSON.stringify({
888
+ schemaVersion: PROMPTFRAME_WORKSPACE_SCHEMA_VERSION,
889
+ components,
890
+ }, null, 2)}\n`, 'utf8');
891
+ }
892
+ function assertNoDuplicateWorkspaceComponents(components) {
893
+ const ids = new Set();
894
+ const paths = new Set();
895
+ for (const component of components) {
896
+ if (ids.has(component.id)) {
897
+ fail(`Duplicate PromptFrame workspace component id: ${component.id}`, 'workspace.component.id_duplicate', 2);
898
+ }
899
+ if (paths.has(component.path)) {
900
+ fail(`Duplicate PromptFrame workspace component path: ${component.path}`, 'workspace.component.path_duplicate', 2);
901
+ }
902
+ ids.add(component.id);
903
+ paths.add(component.path);
904
+ }
905
+ }
906
+ function normalizeWorkspacePath(rawPath) {
907
+ const normalized = rawPath
908
+ .replaceAll('\\', '/')
909
+ .replace(/^\.\/+/, '')
910
+ .replace(/\/+$/, '');
911
+ if (!normalized) {
912
+ fail('PromptFrame workspace component path is required.', 'workspace.component.path_missing', 2);
913
+ }
914
+ if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized)) {
915
+ fail(`PromptFrame workspace component path must be relative: ${rawPath}`, 'workspace.component.path_absolute', 2);
916
+ }
917
+ const segments = normalized.split('/');
918
+ if (segments.some((segment) => !segment || segment === '.' || segment === '..')) {
919
+ fail(`PromptFrame workspace component path must not contain empty, "." or ".." segments: ${rawPath}`, 'workspace.component.path_unsafe', 2);
920
+ }
921
+ return normalized;
922
+ }
923
+ function assertWorkspacePathInsideRoot(root, absolutePath, displayPath) {
924
+ const relativePath = relative(root, absolutePath);
925
+ if (relativePath.startsWith('..') || relativePath === '' || /^[A-Za-z]:/.test(relativePath)) {
926
+ fail(`PromptFrame workspace component path must stay inside the workspace root: ${displayPath}`, 'workspace.component.path_unsafe', 2);
927
+ }
928
+ }
929
+ function buildSourceMetadataHeaders(source) {
930
+ if (source.mode !== 'workspace')
931
+ return {};
932
+ return {
933
+ 'x-promptframe-source-mode': 'workspace',
934
+ 'x-promptframe-source-workspace-config': source.workspaceConfig,
935
+ 'x-promptframe-source-workspace-component-id': source.workspaceComponentId,
936
+ 'x-promptframe-source-component-path': source.componentPath,
937
+ 'x-promptframe-source-manifest-id': source.manifestId,
938
+ };
939
+ }
940
+ const POSITIONAL_VALUE_FLAGS = new Set([
941
+ '--allowed-upload-target',
942
+ '--auth-permissions',
943
+ '--auth-roles',
944
+ '--component-path',
945
+ '--component-id',
946
+ '--config',
947
+ '--description',
948
+ '--display-name',
949
+ '--endpoint',
950
+ '--expires-at',
951
+ '--host',
952
+ '--id',
953
+ '--level',
954
+ '--name',
955
+ '--out',
956
+ '--poll-interval-seconds',
957
+ '--port',
958
+ '--provider',
959
+ '--provider-kind',
960
+ '--provider-name',
961
+ '--project-id',
962
+ '--project-context',
963
+ '--project-namespace',
964
+ '--reason',
965
+ '--scope',
966
+ '--session-id',
967
+ '--target',
968
+ '--tenant-id',
969
+ '--timeout-seconds',
970
+ '--token',
971
+ '--upload-target',
972
+ '--user-id',
973
+ '--workspace-config',
974
+ '--workspace-component',
975
+ ]);
976
+ function componentPathArg(argv) {
977
+ return positionalArgs(argv)[0];
978
+ }
979
+ function positionalArgs(argv) {
980
+ const result = [];
981
+ for (let index = 0; index < argv.length; index += 1) {
982
+ const arg = argv[index];
983
+ if (arg.startsWith('--')) {
984
+ if (!arg.includes('=') && POSITIONAL_VALUE_FLAGS.has(arg))
985
+ index += 1;
986
+ continue;
987
+ }
988
+ result.push(arg);
989
+ }
990
+ return result;
991
+ }
576
992
  function extractRemoteStandardSourceHash(payload) {
577
993
  return stringValue(payload.sourceHash)
578
994
  ?? stringValue(payload.standardSourceHash)
@@ -593,9 +1009,13 @@ async function showStatus(argv) {
593
1009
  };
594
1010
  if (hasFlag(argv, '--json')) {
595
1011
  printJson(output);
596
- return;
597
1012
  }
598
- printBuildSummary(output);
1013
+ else {
1014
+ printBuildSummary(output);
1015
+ }
1016
+ if (hasFlag(argv, '--fail-on-build-failed')) {
1017
+ failIfBuildFailed(output);
1018
+ }
599
1019
  }
600
1020
  async function reindexEvidence(argv) {
601
1021
  const buildId = requiredArg(argv[0], 'reindex requires a build id', 'reindex.build_id.missing');
@@ -817,6 +1237,54 @@ async function discovery(argv) {
817
1237
  if (uploadTargets.length > 0)
818
1238
  console.log(`Upload targets: ${uploadTargets.join(', ')}`);
819
1239
  }
1240
+ async function initProjectContext(argv) {
1241
+ const root = resolve(componentPathArg(argv) ?? '.');
1242
+ const contextPath = join(root, PROMPTFRAME_PROJECT_CONTEXT_FILE_NAME);
1243
+ if (existsSync(contextPath) && !hasFlag(argv, '--force')) {
1244
+ fail(`PromptFrame project context already exists at ${contextPath}. Use --force to overwrite.`, 'project_context.exists', 2);
1245
+ }
1246
+ const endpoint = resolveEndpoint('init', argv);
1247
+ const payload = await fetchProjects(endpoint, argv);
1248
+ const projects = arrayValue(payload.projects).map((item) => asRecord(item)).filter((item) => Boolean(item));
1249
+ const requestedProjectId = valueAfter(argv, '--project-id');
1250
+ const currentProjectId = requestedProjectId ?? stringValue(payload.currentProjectId);
1251
+ const project = projects.find((item) => stringValue(item.projectId) === currentProjectId || (!requestedProjectId && item.isCurrent === true));
1252
+ if (!project) {
1253
+ fail(requestedProjectId
1254
+ ? `PromptFrame project ${requestedProjectId} is not available for this credential.`
1255
+ : 'PromptFrame did not report an active project for this credential.', 'project_context.project_missing', 2);
1256
+ }
1257
+ const projectId = requiredArg(stringValue(project.projectId), 'PromptFrame project response did not include projectId.', 'project_context.project_id_missing');
1258
+ const tenantId = stringValue(project.tenantId);
1259
+ const context = buildProjectContext({
1260
+ endpoint,
1261
+ tenantId,
1262
+ projectId,
1263
+ projectNamespace: valueAfter(argv, '--project-namespace')
1264
+ ?? stringValue(project.projectNamespace)
1265
+ ?? normalizeProjectNamespace(projectId),
1266
+ defaultUploadTarget: valueAfter(argv, '--target')
1267
+ ?? valueAfter(argv, '--upload-target')
1268
+ ?? 'marketplace_authoring',
1269
+ workspaceConfig: valueAfter(argv, '--workspace-config') ?? 'promptframe-workspace.json',
1270
+ });
1271
+ mkdirSync(root, { recursive: true });
1272
+ writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, 'utf8');
1273
+ const output = {
1274
+ command: 'init',
1275
+ endpoint,
1276
+ contextPath,
1277
+ context,
1278
+ project,
1279
+ diagnostic: diagnostic('project_context.init.completed', 'info', 'PromptFrame project context written.'),
1280
+ };
1281
+ if (hasFlag(argv, '--json')) {
1282
+ printJson(output);
1283
+ return;
1284
+ }
1285
+ console.log(`Wrote PromptFrame project context: ${contextPath}`);
1286
+ console.log(`Project: ${projectId}`);
1287
+ }
820
1288
  async function project(argv) {
821
1289
  const subcommand = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'list';
822
1290
  if (subcommand !== 'list' && subcommand !== 'current') {
@@ -873,6 +1341,84 @@ async function fetchProjects(endpoint, argv) {
873
1341
  headers: buildRemoteHeaders(endpoint, argv),
874
1342
  }, 'project.http.failed');
875
1343
  }
1344
+ async function component(argv) {
1345
+ const subcommand = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'list';
1346
+ const rest = argv.slice(argv[0] && !argv[0].startsWith('--') ? 1 : 0);
1347
+ switch (subcommand) {
1348
+ case 'list':
1349
+ await componentList(rest);
1350
+ break;
1351
+ case 'create':
1352
+ await componentCreate(rest);
1353
+ break;
1354
+ default:
1355
+ fail(`Unknown component command: ${subcommand}`, 'component.command.unknown', 2);
1356
+ }
1357
+ }
1358
+ async function componentList(argv) {
1359
+ const endpoint = resolveEndpoint('component', argv);
1360
+ const params = new URLSearchParams();
1361
+ const componentId = valueAfter(argv, '--component-id') ?? positionalArgs(argv)[0];
1362
+ if (componentId)
1363
+ params.set('componentId', assertComponentId(componentId));
1364
+ const query = params.toString();
1365
+ const payload = await fetchJson(`${endpoint}/components/marketplace/self/components${query ? `?${query}` : ''}`, {
1366
+ headers: buildRemoteHeaders(endpoint, argv),
1367
+ }, 'component.list.http.failed');
1368
+ const output = {
1369
+ ...payload,
1370
+ command: 'component.list',
1371
+ endpoint,
1372
+ diagnostic: diagnostic('component.list.completed', 'info', 'PromptFrame project components listed.'),
1373
+ };
1374
+ if (hasFlag(argv, '--json')) {
1375
+ printJson(output);
1376
+ return;
1377
+ }
1378
+ for (const item of arrayValue(payload.declarations)) {
1379
+ const declaration = asRecord(item);
1380
+ if (declaration)
1381
+ printComponentDeclarationLine(declaration);
1382
+ }
1383
+ }
1384
+ async function componentCreate(argv) {
1385
+ const endpoint = resolveEndpoint('component', argv);
1386
+ const componentId = assertComponentId(requiredArg(positionalArgs(argv)[0], 'component create requires <component-id>.', 'component.id_missing'));
1387
+ const displayName = requiredArg(valueAfter(argv, '--display-name') ?? valueAfter(argv, '--name') ?? displayNameFromComponentId(componentId), 'component create requires --display-name <name>.', 'component.display_name_missing');
1388
+ const description = valueAfter(argv, '--description');
1389
+ const payload = compactRecord({
1390
+ componentId,
1391
+ displayName,
1392
+ description,
1393
+ });
1394
+ const response = await fetchJson(`${endpoint}/components/marketplace/self/components`, {
1395
+ method: 'POST',
1396
+ headers: {
1397
+ ...buildRemoteHeaders(endpoint, argv),
1398
+ 'content-type': 'application/json',
1399
+ },
1400
+ body: JSON.stringify(payload),
1401
+ }, 'component.create.http.failed');
1402
+ const output = {
1403
+ ...response,
1404
+ command: 'component.create',
1405
+ endpoint,
1406
+ diagnostic: diagnostic('component.create.completed', 'info', 'PromptFrame project component declared.'),
1407
+ };
1408
+ if (hasFlag(argv, '--json')) {
1409
+ printJson(output);
1410
+ return;
1411
+ }
1412
+ const declaration = asRecord(response.declaration);
1413
+ if (declaration)
1414
+ printComponentDeclarationLine(declaration);
1415
+ }
1416
+ function printComponentDeclarationLine(declaration) {
1417
+ const componentId = stringValue(declaration.componentId) ?? 'unknown';
1418
+ const status = stringValue(declaration.status) ?? 'unknown';
1419
+ const displayName = stringValue(declaration.displayName) ?? componentId;
1420
+ console.log(`${componentId}\t${status}\t${displayName}`);
1421
+ }
876
1422
  function printProjectLine(project, current) {
877
1423
  const prefix = current ? '*' : '-';
878
1424
  const projectId = stringValue(project.projectId) ?? 'unknown';
@@ -1068,16 +1614,22 @@ function setupCi(argv) {
1068
1614
  fail(`Unsupported CI provider: ${provider}.`, 'setup_ci.provider.unsupported', 2);
1069
1615
  }
1070
1616
  const targetDir = resolve(setupCiTargetDir(argv));
1071
- const workflowPath = join(targetDir, '.github', 'workflows', 'promptframe-component.yml');
1617
+ const workspaceMode = hasFlag(argv, '--workspace');
1618
+ const workspaceReports = workspaceMode ? collectWorkspaceComponentReports(targetDir) : [];
1619
+ const workflowPath = join(targetDir, '.github', 'workflows', workspaceMode ? 'promptframe-workspace.yml' : 'promptframe-component.yml');
1072
1620
  if (existsSync(workflowPath) && !hasFlag(argv, '--force')) {
1073
1621
  fail(`PromptFrame GitHub workflow already exists at ${workflowPath}. Use --force to overwrite.`, 'setup_ci.workflow.exists');
1074
1622
  }
1075
1623
  mkdirSync(dirname(workflowPath), { recursive: true });
1076
- writeFileSync(workflowPath, promptFrameGithubWorkflow(), 'utf8');
1624
+ writeFileSync(workflowPath, workspaceMode ? promptFrameGithubWorkspaceWorkflow(workspaceReports) : promptFrameGithubWorkflow(), 'utf8');
1077
1625
  const output = {
1078
1626
  success: true,
1079
1627
  command: 'setup-ci',
1080
1628
  provider,
1629
+ ...(workspaceMode ? {
1630
+ workspace: true,
1631
+ components: workspaceReports.map(({ id, path }) => ({ id, path })),
1632
+ } : {}),
1081
1633
  workflowPath,
1082
1634
  requiredSecrets: ['PROMPTFRAME_CI_TOKEN'],
1083
1635
  requiredVariables: ['PROMPTFRAME_API_BASE'],
@@ -1104,6 +1656,186 @@ function setupCiTargetDir(argv) {
1104
1656
  }
1105
1657
  return '.';
1106
1658
  }
1659
+ function promptFrameGithubWorkspaceWorkflow(components) {
1660
+ const apiBaseVariable = '$' + '{{ vars.PROMPTFRAME_API_BASE }}';
1661
+ const ciTokenSecret = '$' + '{{ secrets.PROMPTFRAME_CI_TOKEN }}';
1662
+ const matrixComponentId = '$' + '{{ matrix.component.componentId }}';
1663
+ const matrixComponentPath = '$' + '{{ matrix.component.componentPath }}';
1664
+ const matrixArtifactName = '$' + '{{ matrix.component.artifactName }}';
1665
+ const matrixItems = components.map((component) => [
1666
+ ' - componentId: ' + JSON.stringify(component.id),
1667
+ ' componentPath: ' + JSON.stringify(component.path),
1668
+ ' artifactName: ' + JSON.stringify(githubMatrixArtifactName(component.id)),
1669
+ ].join('\n')).join('\n');
1670
+ return `name: PromptFrame Component Workspace
1671
+
1672
+ on:
1673
+ pull_request:
1674
+ push:
1675
+ branches: [main]
1676
+ tags:
1677
+ - 'v*'
1678
+ - '*@*'
1679
+ - 'component-*'
1680
+
1681
+ permissions:
1682
+ contents: read
1683
+
1684
+ jobs:
1685
+ check:
1686
+ name: PromptFrame check (${matrixComponentId})
1687
+ runs-on: ubuntu-latest
1688
+ strategy:
1689
+ fail-fast: false
1690
+ matrix:
1691
+ component:
1692
+ ${matrixItems}
1693
+ steps:
1694
+ - uses: actions/checkout@v4
1695
+ - uses: actions/setup-node@v4
1696
+ with:
1697
+ node-version: '20'
1698
+ cache: npm
1699
+ - run: npm install
1700
+ - name: Validate workspace
1701
+ run: npx promptframe workspace validate . --json | tee promptframe-workspace-report.json
1702
+ - name: Validate component package
1703
+ env:
1704
+ PROMPTFRAME_API_BASE: ${apiBaseVariable}
1705
+ COMPONENT_ID: ${matrixComponentId}
1706
+ COMPONENT_PATH: ${matrixComponentPath}
1707
+ run: |
1708
+ set +e
1709
+ npx promptframe check . --workspace-component "$COMPONENT_ID" --json > promptframe-check.json
1710
+ CHECK_EXIT=$?
1711
+ set -e
1712
+ node <<'NODE'
1713
+ const fs = require('node:fs');
1714
+ const payload = JSON.parse(fs.readFileSync('promptframe-check.json', 'utf8'));
1715
+ const diagnostics = Array.isArray(payload.diagnostics) ? payload.diagnostics : [];
1716
+ for (const item of diagnostics) {
1717
+ const code = String(item.code || 'promptframe.diagnostic');
1718
+ const message = String(item.message || code).replace(/\\r?\\n/g, ' ');
1719
+ if (item.severity === 'error') {
1720
+ console.log(\`::error title=\${code}::\${message}\`);
1721
+ } else {
1722
+ console.log(\`::warning title=\${code}::\${message}\`);
1723
+ }
1724
+ }
1725
+ const summary = process.env.GITHUB_STEP_SUMMARY;
1726
+ if (summary) {
1727
+ fs.appendFileSync(summary, [
1728
+ '### PromptFrame workspace check',
1729
+ '',
1730
+ \`- Component ID: \\\`\${process.env.COMPONENT_ID || payload.source?.workspaceComponentId || 'unknown'}\\\`\`,
1731
+ \`- Component path: \\\`\${process.env.COMPONENT_PATH || payload.source?.componentPath || 'unknown'}\\\`\`,
1732
+ \`- Diagnostic: \\\`\${payload.diagnostic?.code || 'unknown'}\\\`\`,
1733
+ \`- Freshness: \\\`\${payload.freshness?.status || 'unknown'}\\\`\`,
1734
+ \`- Diagnostics: \\\`\${diagnostics.length}\\\`\`,
1735
+ '',
1736
+ ].join('\\n'));
1737
+ }
1738
+ NODE
1739
+ exit "$CHECK_EXIT"
1740
+ - uses: actions/upload-artifact@v4
1741
+ if: always()
1742
+ with:
1743
+ name: promptframe-check-report-${matrixArtifactName}
1744
+ path: |
1745
+ promptframe-workspace-report.json
1746
+ promptframe-check.json
1747
+
1748
+ upload:
1749
+ name: PromptFrame upload (${matrixComponentId})
1750
+ if: github.event_name == 'push'
1751
+ needs: check
1752
+ runs-on: ubuntu-latest
1753
+ strategy:
1754
+ fail-fast: false
1755
+ matrix:
1756
+ component:
1757
+ ${matrixItems}
1758
+ steps:
1759
+ - uses: actions/checkout@v4
1760
+ - uses: actions/setup-node@v4
1761
+ with:
1762
+ node-version: '20'
1763
+ cache: npm
1764
+ - run: npm install
1765
+ - name: Upload component to PromptFrame
1766
+ env:
1767
+ PROMPTFRAME_API_BASE: ${apiBaseVariable}
1768
+ PROMPTFRAME_CI_TOKEN: ${ciTokenSecret}
1769
+ COMPONENT_ID: ${matrixComponentId}
1770
+ COMPONENT_PATH: ${matrixComponentPath}
1771
+ run: |
1772
+ set -euo pipefail
1773
+ npx promptframe upload . --workspace-component "$COMPONENT_ID" --endpoint "$PROMPTFRAME_API_BASE" --json | tee promptframe-upload.json
1774
+ BUILD_ID=$(node <<'NODE'
1775
+ const fs = require('node:fs');
1776
+ const payload = JSON.parse(fs.readFileSync('promptframe-upload.json', 'utf8'));
1777
+ process.stdout.write(String(payload.jobId || payload.buildId || payload.build?.buildId || ''));
1778
+ NODE
1779
+ )
1780
+ if [ -z "$BUILD_ID" ]; then
1781
+ echo "PromptFrame upload response did not include a build id." >&2
1782
+ exit 1
1783
+ fi
1784
+ set +e
1785
+ npx promptframe status "$BUILD_ID" --endpoint "$PROMPTFRAME_API_BASE" --json --fail-on-build-failed | tee promptframe-status.json
1786
+ STATUS_EXIT=\${PIPESTATUS[0]}
1787
+ set -e
1788
+ node <<'NODE'
1789
+ const fs = require('node:fs');
1790
+ const payload = JSON.parse(fs.readFileSync('promptframe-upload.json', 'utf8'));
1791
+ const status = JSON.parse(fs.readFileSync('promptframe-status.json', 'utf8'));
1792
+ const build = status.build || {};
1793
+ const buildId = payload.jobId || payload.buildId || build.buildId || 'unknown';
1794
+ const buildStatus = String(build.status || status.status || payload.status || 'queued');
1795
+ const diagnostics = Array.isArray(build.diagnostics) ? build.diagnostics : [];
1796
+ const firstError = diagnostics.find((item) => item && item.severity === 'error') || diagnostics[0] || {};
1797
+ if (['failed', 'cancelled', 'canceled'].includes(buildStatus.toLowerCase())) {
1798
+ const message = [
1799
+ \`buildId=\${buildId}\`,
1800
+ \`status=\${buildStatus}\`,
1801
+ firstError.code ? \`code=\${firstError.code}\` : '',
1802
+ firstError.message ? \`message=\${String(firstError.message).replace(/\\r?\\n/g, ' ').slice(0, 300)}\` : '',
1803
+ ].filter(Boolean).join(' ');
1804
+ console.log(\`::error title=PromptFrame platform build failed::\${message}\`);
1805
+ }
1806
+ const summary = process.env.GITHUB_STEP_SUMMARY;
1807
+ if (summary) {
1808
+ fs.appendFileSync(summary, [
1809
+ '### PromptFrame workspace upload',
1810
+ '',
1811
+ \`- Component ID: \\\`\${process.env.COMPONENT_ID || payload.source?.workspaceComponentId || 'unknown'}\\\`\`,
1812
+ \`- Component path: \\\`\${process.env.COMPONENT_PATH || payload.source?.componentPath || 'unknown'}\\\`\`,
1813
+ \`- Diagnostic: \\\`\${payload.diagnostic?.code || 'unknown'}\\\`\`,
1814
+ \`- Build ID: \\\`\${buildId}\\\`\`,
1815
+ \`- Platform build status: \\\`\${buildStatus}\\\`\`,
1816
+ firstError.code ? \`- Top diagnostic: \\\`\${firstError.code}\\\`\` : '- Top diagnostic: none',
1817
+ firstError.message ? \`- Message: \${String(firstError.message).replace(/\\r?\\n/g, ' ').slice(0, 300)}\` : '',
1818
+ '',
1819
+ ].filter(Boolean).join('\\n'));
1820
+ }
1821
+ NODE
1822
+ exit "$STATUS_EXIT"
1823
+ - uses: actions/upload-artifact@v4
1824
+ if: always()
1825
+ with:
1826
+ name: promptframe-upload-report-${matrixArtifactName}
1827
+ path: |
1828
+ promptframe-upload.json
1829
+ promptframe-status.json
1830
+ `;
1831
+ }
1832
+ function githubMatrixArtifactName(componentId) {
1833
+ return componentId
1834
+ .replace(/^@/, '')
1835
+ .replace(/[^A-Za-z0-9_.-]+/g, '-')
1836
+ .replace(/^-+|-+$/g, '')
1837
+ || 'component';
1838
+ }
1107
1839
  function promptFrameGithubWorkflow() {
1108
1840
  const apiBaseVariable = '$' + '{{ vars.PROMPTFRAME_API_BASE }}';
1109
1841
  const ciTokenSecret = '$' + '{{ secrets.PROMPTFRAME_CI_TOKEN }}';
@@ -1200,23 +1932,43 @@ jobs:
1200
1932
  echo "PromptFrame upload response did not include a build id." >&2
1201
1933
  exit 1
1202
1934
  fi
1203
- npx promptframe status "$BUILD_ID" --endpoint "$PROMPTFRAME_API_BASE" --json | tee promptframe-status.json
1935
+ set +e
1936
+ npx promptframe status "$BUILD_ID" --endpoint "$PROMPTFRAME_API_BASE" --json --fail-on-build-failed | tee promptframe-status.json
1937
+ STATUS_EXIT=\${PIPESTATUS[0]}
1938
+ set -e
1204
1939
  node <<'NODE'
1205
1940
  const fs = require('node:fs');
1206
1941
  const payload = JSON.parse(fs.readFileSync('promptframe-upload.json', 'utf8'));
1207
1942
  const status = JSON.parse(fs.readFileSync('promptframe-status.json', 'utf8'));
1943
+ const build = status.build || {};
1944
+ const buildId = payload.jobId || payload.buildId || build.buildId || 'unknown';
1945
+ const buildStatus = String(build.status || status.status || payload.status || 'queued');
1946
+ const diagnostics = Array.isArray(build.diagnostics) ? build.diagnostics : [];
1947
+ const firstError = diagnostics.find((item) => item && item.severity === 'error') || diagnostics[0] || {};
1948
+ if (['failed', 'cancelled', 'canceled'].includes(buildStatus.toLowerCase())) {
1949
+ const message = [
1950
+ \`buildId=\${buildId}\`,
1951
+ \`status=\${buildStatus}\`,
1952
+ firstError.code ? \`code=\${firstError.code}\` : '',
1953
+ firstError.message ? \`message=\${String(firstError.message).replace(/\\r?\\n/g, ' ').slice(0, 300)}\` : '',
1954
+ ].filter(Boolean).join(' ');
1955
+ console.log(\`::error title=PromptFrame platform build failed::\${message}\`);
1956
+ }
1208
1957
  const summary = process.env.GITHUB_STEP_SUMMARY;
1209
1958
  if (summary) {
1210
1959
  fs.appendFileSync(summary, [
1211
1960
  '### PromptFrame upload',
1212
1961
  '',
1213
1962
  \`- Diagnostic: \\\`\${payload.diagnostic?.code || 'unknown'}\\\`\`,
1214
- \`- Build ID: \\\`\${payload.jobId || payload.buildId || 'unknown'}\\\`\`,
1215
- \`- Admission status: \\\`\${status.build?.status || status.status || payload.status || 'queued'}\\\`\`,
1963
+ \`- Build ID: \\\`\${buildId}\\\`\`,
1964
+ \`- Platform build status: \\\`\${buildStatus}\\\`\`,
1965
+ firstError.code ? \`- Top diagnostic: \\\`\${firstError.code}\\\`\` : '- Top diagnostic: none',
1966
+ firstError.message ? \`- Message: \${String(firstError.message).replace(/\\r?\\n/g, ' ').slice(0, 300)}\` : '',
1216
1967
  '',
1217
- ].join('\\n'));
1968
+ ].filter(Boolean).join('\\n'));
1218
1969
  }
1219
1970
  NODE
1971
+ exit "$STATUS_EXIT"
1220
1972
  - uses: actions/upload-artifact@v4
1221
1973
  if: always()
1222
1974
  with:
@@ -1228,21 +1980,29 @@ jobs:
1228
1980
  }
1229
1981
  function resolveEndpoint(commandName, argv) {
1230
1982
  const config = readConfig(argv);
1983
+ const credential = asRecord(config.credential);
1984
+ const projectContext = readProjectContext(argv);
1231
1985
  const endpoint = valueAfter(argv, '--endpoint')
1232
1986
  ?? process.env.PROMPTFRAME_API_BASE
1233
1987
  ?? process.env.REMOTION_MEDIA_API_BASE
1234
- ?? stringValue(config.endpoint);
1988
+ ?? stringValue(config.endpoint)
1989
+ ?? stringValue(credential?.endpoint)
1990
+ ?? projectContext?.endpoint;
1235
1991
  if (!endpoint) {
1236
- 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);
1992
+ 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);
1237
1993
  }
1238
1994
  return normalizeEndpoint(endpoint);
1239
1995
  }
1240
1996
  function resolveOptionalEndpoint(argv) {
1241
1997
  const config = readConfig(argv);
1998
+ const credential = asRecord(config.credential);
1999
+ const projectContext = readProjectContext(argv);
1242
2000
  const endpoint = valueAfter(argv, '--endpoint')
1243
2001
  ?? stringValue(process.env.PROMPTFRAME_API_BASE)
1244
2002
  ?? stringValue(process.env.REMOTION_MEDIA_API_BASE)
1245
- ?? stringValue(config.endpoint);
2003
+ ?? stringValue(config.endpoint)
2004
+ ?? stringValue(credential?.endpoint)
2005
+ ?? projectContext?.endpoint;
1246
2006
  return endpoint ? normalizeEndpoint(endpoint) : undefined;
1247
2007
  }
1248
2008
  function buildContextHeaders(argv) {
@@ -1271,7 +2031,7 @@ function buildRemoteHeaders(endpoint, argv, options = {}) {
1271
2031
  'x-auth-permissions',
1272
2032
  ].includes(name));
1273
2033
  if (isFormalEndpoint(endpoint) && hasDevHeaders) {
1274
- 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');
2034
+ 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');
1275
2035
  }
1276
2036
  if (isFormalEndpoint(endpoint) && options.requireAuth !== false) {
1277
2037
  fail('No PromptFrame CLI credential found for this endpoint. Run promptframe login --endpoint <url> or provide PROMPTFRAME_CI_TOKEN.', 'cli.auth.login_required', 2);
@@ -1411,6 +2171,110 @@ function writeConfig(argv, config) {
1411
2171
  function resolveConfigPath(argv) {
1412
2172
  return resolve(valueAfter(argv, '--config') ?? process.env.PROMPTFRAME_CONFIG ?? join(os.homedir(), '.promptframe', 'component-authoring.json'));
1413
2173
  }
2174
+ function readProjectContext(argv) {
2175
+ const explicitPath = valueAfter(argv, '--project-context');
2176
+ const contextPath = explicitPath
2177
+ ? resolve(explicitPath)
2178
+ : findProjectContextPath(process.cwd());
2179
+ if (!contextPath || !existsSync(contextPath))
2180
+ return undefined;
2181
+ let raw;
2182
+ try {
2183
+ raw = JSON.parse(readFileSync(contextPath, 'utf8'));
2184
+ }
2185
+ catch {
2186
+ fail(`PromptFrame project context is not valid JSON: ${contextPath}`, 'project_context.invalid_json', 2);
2187
+ }
2188
+ return parseProjectContext(raw, contextPath);
2189
+ }
2190
+ function findProjectContextPath(startDir) {
2191
+ let current = resolve(startDir);
2192
+ for (;;) {
2193
+ const candidate = join(current, PROMPTFRAME_PROJECT_CONTEXT_FILE_NAME);
2194
+ if (existsSync(candidate))
2195
+ return candidate;
2196
+ const parent = dirname(current);
2197
+ if (parent === current)
2198
+ return undefined;
2199
+ current = parent;
2200
+ }
2201
+ }
2202
+ function parseProjectContext(value, sourcePath) {
2203
+ const record = asRecord(value);
2204
+ if (!record) {
2205
+ fail(`PromptFrame project context must be a JSON object: ${sourcePath}`, 'project_context.invalid', 2);
2206
+ }
2207
+ assertProjectContextSecretFree(record, sourcePath);
2208
+ const schemaVersion = stringValue(record.schemaVersion);
2209
+ if (schemaVersion && schemaVersion !== PROMPTFRAME_PROJECT_CONTEXT_SCHEMA_VERSION) {
2210
+ fail(`Unsupported PromptFrame project context schemaVersion ${schemaVersion}. Expected ${PROMPTFRAME_PROJECT_CONTEXT_SCHEMA_VERSION}.`, 'project_context.schema_unsupported', 2);
2211
+ }
2212
+ return {
2213
+ schemaVersion: PROMPTFRAME_PROJECT_CONTEXT_SCHEMA_VERSION,
2214
+ ...(stringValue(record.endpoint) ? { endpoint: normalizeEndpoint(stringValue(record.endpoint)) } : {}),
2215
+ ...(stringValue(record.tenantId) ? { tenantId: stringValue(record.tenantId) } : {}),
2216
+ ...(stringValue(record.projectId) ? { projectId: stringValue(record.projectId) } : {}),
2217
+ ...(stringValue(record.projectNamespace) ? { projectNamespace: stringValue(record.projectNamespace) } : {}),
2218
+ ...(stringValue(record.defaultUploadTarget) ? { defaultUploadTarget: stringValue(record.defaultUploadTarget) } : {}),
2219
+ ...(stringValue(record.workspaceConfig) ? { workspaceConfig: stringValue(record.workspaceConfig) } : {}),
2220
+ };
2221
+ }
2222
+ function buildProjectContext(input) {
2223
+ const context = {
2224
+ schemaVersion: PROMPTFRAME_PROJECT_CONTEXT_SCHEMA_VERSION,
2225
+ endpoint: normalizeEndpoint(input.endpoint),
2226
+ ...(input.tenantId ? { tenantId: input.tenantId } : {}),
2227
+ projectId: input.projectId,
2228
+ ...(input.projectNamespace ? { projectNamespace: normalizeProjectNamespace(input.projectNamespace) } : {}),
2229
+ ...(input.defaultUploadTarget ? { defaultUploadTarget: input.defaultUploadTarget } : {}),
2230
+ ...(input.workspaceConfig ? { workspaceConfig: input.workspaceConfig } : {}),
2231
+ };
2232
+ assertProjectContextSecretFree(context, PROMPTFRAME_PROJECT_CONTEXT_FILE_NAME);
2233
+ return context;
2234
+ }
2235
+ function assertProjectContextSecretFree(value, sourcePath) {
2236
+ if (!value || typeof value !== 'object')
2237
+ return;
2238
+ if (Array.isArray(value)) {
2239
+ for (const item of value)
2240
+ assertProjectContextSecretFree(item, sourcePath);
2241
+ return;
2242
+ }
2243
+ for (const [key, child] of Object.entries(value)) {
2244
+ const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, '');
2245
+ if (PROJECT_CONTEXT_FORBIDDEN_KEY_NAMES.has(normalizedKey)) {
2246
+ 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);
2247
+ }
2248
+ assertProjectContextSecretFree(child, sourcePath);
2249
+ }
2250
+ }
2251
+ function assertComponentId(value) {
2252
+ if (!/^@[a-z0-9][a-z0-9-]{1,62}\/[a-z0-9][a-z0-9-]{1,62}$/.test(value)) {
2253
+ fail(`Invalid component id "${value}". Expected @project-namespace/component-slug.`, 'component.id_invalid', 2);
2254
+ }
2255
+ return value;
2256
+ }
2257
+ function normalizeProjectNamespace(value) {
2258
+ const normalized = value
2259
+ .trim()
2260
+ .toLowerCase()
2261
+ .replace(/^@/, '')
2262
+ .replace(/[^a-z0-9-]+/g, '-')
2263
+ .replace(/^-+|-+$/g, '')
2264
+ .slice(0, 63);
2265
+ if (!/^[a-z0-9][a-z0-9-]{1,62}$/.test(normalized)) {
2266
+ fail(`Invalid project namespace "${value}".`, 'project_context.namespace_invalid', 2);
2267
+ }
2268
+ return normalized;
2269
+ }
2270
+ function displayNameFromComponentId(componentId) {
2271
+ const slug = componentId.split('/').at(-1) ?? componentId;
2272
+ return slug
2273
+ .split('-')
2274
+ .filter(Boolean)
2275
+ .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)
2276
+ .join(' ');
2277
+ }
1414
2278
  async function fetchJson(url, init, code) {
1415
2279
  const response = await fetch(url, init);
1416
2280
  const payload = await response.json().catch(() => ({}));
@@ -2095,6 +2959,26 @@ function printBuildSummary(payload) {
2095
2959
  console.log(`${stringValue(diagnosticItem?.severity)?.toUpperCase() ?? 'INFO'} ${stringValue(diagnosticItem?.code) ?? 'unknown'}: ${stringValue(diagnosticItem?.message) ?? ''}`);
2096
2960
  }
2097
2961
  }
2962
+ function failIfBuildFailed(payload) {
2963
+ const status = getBuildStatus(payload);
2964
+ if (!status || !['failed', 'cancelled', 'canceled'].includes(status.toLowerCase()))
2965
+ return;
2966
+ const buildId = getBuildId(payload) ?? 'unknown';
2967
+ const diagnosticItem = firstBuildDiagnostic(payload);
2968
+ const diagnosticText = diagnosticItem
2969
+ ? `${stringValue(diagnosticItem.code) ?? 'unknown'}: ${stringValue(diagnosticItem.message) ?? ''}`.trim()
2970
+ : 'no diagnostic';
2971
+ fail(`PromptFrame platform build failed: buildId=${buildId} status=${status} diagnostic=${diagnosticText}`, 'status.build.failed', 1);
2972
+ }
2973
+ function getBuildStatus(payload) {
2974
+ return stringValue(asRecord(payload.build)?.status) ?? stringValue(payload.status);
2975
+ }
2976
+ function firstBuildDiagnostic(payload) {
2977
+ const diagnostics = arrayValue(asRecord(payload.build)?.diagnostics)
2978
+ .map((item) => asRecord(item))
2979
+ .filter((item) => Boolean(item));
2980
+ return diagnostics.find((item) => item.severity === 'error') ?? diagnostics[0];
2981
+ }
2098
2982
  function printStatusUrl(endpoint, payload) {
2099
2983
  const statusUrl = stringValue(payload.statusUrl) ?? stringValue(asRecord(payload.build)?.statusUrl);
2100
2984
  if (!statusUrl)
@@ -2143,6 +3027,7 @@ Commands:
2143
3027
  --target <target> marketplace_authoring or project_private_generation
2144
3028
  --target marketplace --strict Alias for strict external marketplace authoring
2145
3029
  status <buildId> Fetch component build status
3030
+ --fail-on-build-failed Exit nonzero when platform build status is failed/cancelled
2146
3031
  reindex <buildId> Rebuild component search/evidence indexes
2147
3032
  probe <buildId> --level <level> Rerun component layout/security probe
2148
3033
  login --endpoint <url> Start browser login code flow and store a CLI token
@@ -2150,13 +3035,15 @@ Commands:
2150
3035
  whoami Show the current PromptFrame CLI identity
2151
3036
  logout Revoke the current CLI token and clear local config
2152
3037
  discovery Fetch platform endpoint and self-service capabilities
3038
+ init [dir] Write secret-free .promptframerc project context
2153
3039
  project list|current List accessible projects or show the active project
3040
+ component list|create List or declare Project-scoped components
2154
3041
  ci-token create|list|revoke Manage self-service CI tokens for the current project
2155
3042
  setup-ci [dir] --provider github Write a GitHub Actions workflow skeleton
2156
3043
  configure --endpoint <url> Write local CLI endpoint/context config
2157
3044
 
2158
3045
  Endpoint resolution:
2159
- --endpoint, PROMPTFRAME_API_BASE, REMOTION_MEDIA_API_BASE, then local config.
3046
+ --endpoint, PROMPTFRAME_API_BASE, REMOTION_MEDIA_API_BASE, local config, then .promptframerc.
2160
3047
  The public CLI embeds no production/private endpoint defaults.
2161
3048
  Auth context:
2162
3049
  promptframe login, PROMPTFRAME_CI_TOKEN, or PROMPTFRAME_CLI_TOKEN for formal endpoints.