primitive-admin 1.1.0-alpha.9 → 1.1.0
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/README.md +523 -109
- package/assets/skill/skills/primitive-platform/SKILL.md +289 -0
- package/dist/bin/primitive.d.ts +2 -0
- package/dist/bin/primitive.js +372 -21
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.d.ts +2 -0
- package/dist/src/commands/admins.js +136 -30
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.d.ts +2 -0
- package/dist/src/commands/analytics.js +713 -55
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.d.ts +2 -0
- package/dist/src/commands/apps.js +65 -100
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth-sessions.d.ts +7 -0
- package/dist/src/commands/auth-sessions.js +144 -0
- package/dist/src/commands/auth-sessions.js.map +1 -0
- package/dist/src/commands/auth.d.ts +2 -0
- package/dist/src/commands/auth.js +238 -108
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.d.ts +2 -0
- package/dist/src/commands/blob-buckets.js +331 -0
- package/dist/src/commands/blob-buckets.js.map +1 -0
- package/dist/src/commands/catalog.d.ts +2 -0
- package/dist/src/commands/catalog.js +63 -48
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.d.ts +2 -0
- package/dist/src/commands/collection-type-configs.js +85 -0
- package/dist/src/commands/collection-type-configs.js.map +1 -0
- package/dist/src/commands/collections.d.ts +2 -0
- package/dist/src/commands/collections.js +1282 -0
- package/dist/src/commands/collections.js.map +1 -0
- package/dist/src/commands/comparisons.d.ts +2 -0
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/config.d.ts +46 -0
- package/dist/src/commands/config.js +465 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/connections.d.ts +2 -0
- package/dist/src/commands/connections.js +99 -0
- package/dist/src/commands/connections.js.map +1 -0
- package/dist/src/commands/cron-triggers.d.ts +2 -0
- package/dist/src/commands/cron-triggers.js +266 -0
- package/dist/src/commands/cron-triggers.js.map +1 -0
- package/dist/src/commands/database-type-configs.d.ts +2 -0
- package/dist/src/commands/database-type-configs.js +164 -0
- package/dist/src/commands/database-type-configs.js.map +1 -0
- package/dist/src/commands/database-types.d.ts +2 -0
- package/dist/src/commands/database-types.js +471 -0
- package/dist/src/commands/database-types.js.map +1 -0
- package/dist/src/commands/databases.d.ts +65 -0
- package/dist/src/commands/databases.js +1887 -243
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +60 -0
- package/dist/src/commands/documents.js +2888 -22
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.d.ts +2 -0
- package/dist/src/commands/email-templates.js +175 -0
- package/dist/src/commands/email-templates.js.map +1 -0
- package/dist/src/commands/env.d.ts +23 -0
- package/dist/src/commands/env.js +395 -0
- package/dist/src/commands/env.js.map +1 -0
- package/dist/src/commands/feature-flags.d.ts +14 -0
- package/dist/src/commands/feature-flags.js +117 -0
- package/dist/src/commands/feature-flags.js.map +1 -0
- package/dist/src/commands/functions.d.ts +20 -0
- package/dist/src/commands/functions.js +1630 -0
- package/dist/src/commands/functions.js.map +1 -0
- package/dist/src/commands/group-type-configs.d.ts +2 -0
- package/dist/src/commands/group-type-configs.js +87 -0
- package/dist/src/commands/group-type-configs.js.map +1 -0
- package/dist/src/commands/groups.d.ts +2 -0
- package/dist/src/commands/groups.js +65 -113
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.d.ts +223 -0
- package/dist/src/commands/guides.js +627 -69
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/init.d.ts +37 -0
- package/dist/src/commands/init.js +1693 -208
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.d.ts +2 -0
- package/dist/src/commands/integrations.js +428 -187
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/llm.d.ts +2 -0
- package/dist/src/commands/llm.js +4 -2
- package/dist/src/commands/llm.js.map +1 -1
- package/dist/src/commands/locks.d.ts +8 -0
- package/dist/src/commands/locks.js +175 -0
- package/dist/src/commands/locks.js.map +1 -0
- package/dist/src/commands/metadata-category-configs.d.ts +12 -0
- package/dist/src/commands/metadata-category-configs.js +113 -0
- package/dist/src/commands/metadata-category-configs.js.map +1 -0
- package/dist/src/commands/metadata.d.ts +2 -0
- package/dist/src/commands/metadata.js +288 -0
- package/dist/src/commands/metadata.js.map +1 -0
- package/dist/src/commands/prompts.d.ts +2 -0
- package/dist/src/commands/prompts.js +322 -634
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +3 -0
- package/dist/src/commands/rule-sets.js +139 -148
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.d.ts +30 -0
- package/dist/src/commands/scripts.js +688 -0
- package/dist/src/commands/scripts.js.map +1 -0
- package/dist/src/commands/secrets.d.ts +2 -0
- package/dist/src/commands/secrets.js +109 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/sessions.d.ts +2 -0
- package/dist/src/commands/sessions.js +76 -0
- package/dist/src/commands/sessions.js.map +1 -0
- package/dist/src/commands/skill.d.ts +2 -0
- package/dist/src/commands/skill.js +29 -0
- package/dist/src/commands/skill.js.map +1 -0
- package/dist/src/commands/sync-app-settings.d.ts +158 -0
- package/dist/src/commands/sync-app-settings.js +328 -0
- package/dist/src/commands/sync-app-settings.js.map +1 -0
- package/dist/src/commands/sync.d.ts +2670 -0
- package/dist/src/commands/sync.js +17144 -834
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.d.ts +2 -0
- package/dist/src/commands/tokens.js +132 -22
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.d.ts +2 -0
- package/dist/src/commands/users.js +542 -24
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.d.ts +8 -0
- package/dist/src/commands/vars.js +97 -0
- package/dist/src/commands/vars.js.map +1 -0
- package/dist/src/commands/waitlist.d.ts +2 -0
- package/dist/src/commands/waitlist.js +12 -11
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.d.ts +31 -0
- package/dist/src/commands/webhooks.js +633 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.d.ts +88 -0
- package/dist/src/commands/workflows.js +1568 -742
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/access-rule-display.d.ts +21 -0
- package/dist/src/lib/access-rule-display.js +34 -0
- package/dist/src/lib/access-rule-display.js.map +1 -0
- package/dist/src/lib/api-client.d.ts +2550 -0
- package/dist/src/lib/api-client.js +2503 -160
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +263 -0
- package/dist/src/lib/app-settings-descriptor.js +583 -0
- package/dist/src/lib/app-settings-descriptor.js.map +1 -0
- package/dist/src/lib/auth-flow.d.ts +8 -0
- package/dist/src/lib/batch.d.ts +26 -0
- package/dist/src/lib/batch.js +32 -0
- package/dist/src/lib/batch.js.map +1 -0
- package/dist/src/lib/block-layout.d.ts +160 -0
- package/dist/src/lib/block-layout.js +451 -0
- package/dist/src/lib/block-layout.js.map +1 -0
- package/dist/src/lib/block-selector.d.ts +58 -0
- package/dist/src/lib/block-selector.js +92 -0
- package/dist/src/lib/block-selector.js.map +1 -0
- package/dist/src/lib/canonical-json.d.ts +12 -0
- package/dist/src/lib/canonical-json.js +35 -0
- package/dist/src/lib/canonical-json.js.map +1 -0
- package/dist/src/lib/channel.d.ts +30 -0
- package/dist/src/lib/channel.js +68 -0
- package/dist/src/lib/channel.js.map +1 -0
- package/dist/src/lib/cli-manifest.d.ts +68 -0
- package/dist/src/lib/cli-manifest.js +71 -0
- package/dist/src/lib/cli-manifest.js.map +1 -0
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +101 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js +191 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
- package/dist/src/lib/codegen-shared/prettierStable.d.ts +262 -0
- package/dist/src/lib/codegen-shared/prettierStable.js +610 -0
- package/dist/src/lib/codegen-shared/prettierStable.js.map +1 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +38 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +46 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
- package/dist/src/lib/collection-export.d.ts +184 -0
- package/dist/src/lib/collection-export.js +252 -0
- package/dist/src/lib/collection-export.js.map +1 -0
- package/dist/src/lib/config-json-field.d.ts +28 -0
- package/dist/src/lib/config-json-field.js +56 -0
- package/dist/src/lib/config-json-field.js.map +1 -0
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +740 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -0
- package/dist/src/lib/config-payload.d.ts +92 -0
- package/dist/src/lib/config-payload.js +161 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +141 -0
- package/dist/src/lib/config-surface.js +368 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/config-toml.d.ts +10 -0
- package/dist/src/lib/config-toml.js +42 -0
- package/dist/src/lib/config-toml.js.map +1 -0
- package/dist/src/lib/config.d.ts +71 -0
- package/dist/src/lib/config.js +71 -68
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/confirm-prompt.d.ts +83 -0
- package/dist/src/lib/confirm-prompt.js +110 -0
- package/dist/src/lib/confirm-prompt.js.map +1 -0
- package/dist/src/lib/constants.d.ts +11 -0
- package/dist/src/lib/constants.js +12 -0
- package/dist/src/lib/constants.js.map +1 -0
- package/dist/src/lib/crash-handlers.d.ts +20 -0
- package/dist/src/lib/crash-handlers.js +49 -0
- package/dist/src/lib/crash-handlers.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +104 -0
- package/dist/src/lib/credentials-store.js +336 -0
- package/dist/src/lib/credentials-store.js.map +1 -0
- package/dist/src/lib/csv.d.ts +47 -0
- package/dist/src/lib/csv.js +172 -0
- package/dist/src/lib/csv.js.map +1 -0
- package/dist/src/lib/data-input.d.ts +23 -0
- package/dist/src/lib/data-input.js +50 -0
- package/dist/src/lib/data-input.js.map +1 -0
- package/dist/src/lib/db-codegen/dbFingerprint.d.ts +10 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
- package/dist/src/lib/db-codegen/dbGenerator.d.ts +67 -0
- package/dist/src/lib/db-codegen/dbGenerator.js +170 -0
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/db-codegen/dbNaming.d.ts +87 -0
- package/dist/src/lib/db-codegen/dbNaming.js +180 -0
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +272 -0
- package/dist/src/lib/db-codegen/dbTemplates.js +480 -0
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +73 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js +139 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js +525 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +112 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +211 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
- package/dist/src/lib/deprecation.d.ts +22 -0
- package/dist/src/lib/deprecation.js +43 -0
- package/dist/src/lib/deprecation.js.map +1 -0
- package/dist/src/lib/document-export-permissions.d.ts +30 -0
- package/dist/src/lib/document-export-permissions.js +54 -0
- package/dist/src/lib/document-export-permissions.js.map +1 -0
- package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
- package/dist/src/lib/document-ingest-artifact.js +505 -0
- package/dist/src/lib/document-ingest-artifact.js.map +1 -0
- package/dist/src/lib/document-ingest-input.d.ts +51 -0
- package/dist/src/lib/document-ingest-input.js +132 -0
- package/dist/src/lib/document-ingest-input.js.map +1 -0
- package/dist/src/lib/document-ingest-rows.d.ts +137 -0
- package/dist/src/lib/document-ingest-rows.js +181 -0
- package/dist/src/lib/document-ingest-rows.js.map +1 -0
- package/dist/src/lib/document-ingest.d.ts +101 -0
- package/dist/src/lib/document-ingest.js +384 -0
- package/dist/src/lib/document-ingest.js.map +1 -0
- package/dist/src/lib/env-resolver-core.d.ts +258 -0
- package/dist/src/lib/env-resolver-core.js +447 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +99 -0
- package/dist/src/lib/env-resolver.js +153 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/fetch.d.ts +5 -0
- package/dist/src/lib/function-bundle.d.ts +147 -0
- package/dist/src/lib/function-bundle.js +341 -0
- package/dist/src/lib/function-bundle.js.map +1 -0
- package/dist/src/lib/function-collect.d.ts +123 -0
- package/dist/src/lib/function-collect.js +610 -0
- package/dist/src/lib/function-collect.js.map +1 -0
- package/dist/src/lib/function-db-types.d.ts +202 -0
- package/dist/src/lib/function-db-types.js +870 -0
- package/dist/src/lib/function-db-types.js.map +1 -0
- package/dist/src/lib/function-document-types.d.ts +144 -0
- package/dist/src/lib/function-document-types.js +370 -0
- package/dist/src/lib/function-document-types.js.map +1 -0
- package/dist/src/lib/function-grants-preflight.d.ts +64 -0
- package/dist/src/lib/function-grants-preflight.js +105 -0
- package/dist/src/lib/function-grants-preflight.js.map +1 -0
- package/dist/src/lib/function-log-lines.d.ts +76 -0
- package/dist/src/lib/function-log-lines.js +160 -0
- package/dist/src/lib/function-log-lines.js.map +1 -0
- package/dist/src/lib/function-log-row.d.ts +29 -0
- package/dist/src/lib/function-log-row.js +73 -0
- package/dist/src/lib/function-log-row.js.map +1 -0
- package/dist/src/lib/function-log-tail.d.ts +132 -0
- package/dist/src/lib/function-log-tail.js +262 -0
- package/dist/src/lib/function-log-tail.js.map +1 -0
- package/dist/src/lib/function-run.d.ts +289 -0
- package/dist/src/lib/function-run.js +389 -0
- package/dist/src/lib/function-run.js.map +1 -0
- package/dist/src/lib/function-schema-codegen.d.ts +143 -0
- package/dist/src/lib/function-schema-codegen.js +420 -0
- package/dist/src/lib/function-schema-codegen.js.map +1 -0
- package/dist/src/lib/function-sync.d.ts +459 -0
- package/dist/src/lib/function-sync.js +1258 -0
- package/dist/src/lib/function-sync.js.map +1 -0
- package/dist/src/lib/function-trigger-listing.d.ts +27 -0
- package/dist/src/lib/function-trigger-listing.js +70 -0
- package/dist/src/lib/function-trigger-listing.js.map +1 -0
- package/dist/src/lib/function-typecheck.d.ts +86 -0
- package/dist/src/lib/function-typecheck.js +370 -0
- package/dist/src/lib/function-typecheck.js.map +1 -0
- package/dist/src/lib/function-versions.d.ts +122 -0
- package/dist/src/lib/function-versions.js +182 -0
- package/dist/src/lib/function-versions.js.map +1 -0
- package/dist/src/lib/generated-allowlist.d.ts +28 -0
- package/dist/src/lib/generated-allowlist.js +281 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +2936 -0
- package/dist/src/lib/generated-config-surfaces.js +10569 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-sdk-types.d.ts +12 -0
- package/dist/src/lib/generated-sdk-types.js +13 -0
- package/dist/src/lib/generated-sdk-types.js.map +1 -0
- package/dist/src/lib/generated-template-lint.d.ts +212 -0
- package/dist/src/lib/generated-template-lint.js +624 -0
- package/dist/src/lib/generated-template-lint.js.map +1 -0
- package/dist/src/lib/init-adopt.d.ts +16 -0
- package/dist/src/lib/init-adopt.js +34 -0
- package/dist/src/lib/init-adopt.js.map +1 -0
- package/dist/src/lib/init-assets.d.ts +39 -0
- package/dist/src/lib/init-assets.js +97 -0
- package/dist/src/lib/init-assets.js.map +1 -0
- package/dist/src/lib/init-client-platforms.d.ts +14 -0
- package/dist/src/lib/init-client-platforms.js +71 -0
- package/dist/src/lib/init-client-platforms.js.map +1 -0
- package/dist/src/lib/init-config.d.ts +98 -0
- package/dist/src/lib/init-config.js +186 -0
- package/dist/src/lib/init-config.js.map +1 -0
- package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
- package/dist/src/lib/init-email-redirect-uris.js +46 -0
- package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
- package/dist/src/lib/init-ios-links.d.ts +91 -0
- package/dist/src/lib/init-ios-links.js +219 -0
- package/dist/src/lib/init-ios-links.js.map +1 -0
- package/dist/src/lib/init-plan.d.ts +80 -0
- package/dist/src/lib/init-plan.js +95 -0
- package/dist/src/lib/init-plan.js.map +1 -0
- package/dist/src/lib/init-production-env.d.ts +48 -0
- package/dist/src/lib/init-production-env.js +59 -0
- package/dist/src/lib/init-production-env.js.map +1 -0
- package/dist/src/lib/init-schema.d.ts +74 -0
- package/dist/src/lib/init-schema.js +358 -0
- package/dist/src/lib/init-schema.js.map +1 -0
- package/dist/src/lib/init-xcode.d.ts +34 -0
- package/dist/src/lib/init-xcode.js +138 -0
- package/dist/src/lib/init-xcode.js.map +1 -0
- package/dist/src/lib/integration-request-config.d.ts +30 -0
- package/dist/src/lib/integration-request-config.js +145 -0
- package/dist/src/lib/integration-request-config.js.map +1 -0
- package/dist/src/lib/integration-selector.d.ts +42 -0
- package/dist/src/lib/integration-selector.js +46 -0
- package/dist/src/lib/integration-selector.js.map +1 -0
- package/dist/src/lib/ios-app-id.d.ts +34 -0
- package/dist/src/lib/ios-app-id.js +69 -0
- package/dist/src/lib/ios-app-id.js.map +1 -0
- package/dist/src/lib/list-options.d.ts +68 -0
- package/dist/src/lib/list-options.js +89 -0
- package/dist/src/lib/list-options.js.map +1 -0
- package/dist/src/lib/local-state.d.ts +55 -0
- package/dist/src/lib/local-state.js +167 -0
- package/dist/src/lib/local-state.js.map +1 -0
- package/dist/src/lib/local-test-cases.d.ts +63 -0
- package/dist/src/lib/local-test-cases.js +136 -0
- package/dist/src/lib/local-test-cases.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +715 -0
- package/dist/src/lib/log-inspection.js +816 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/logout-admin-session.d.ts +33 -0
- package/dist/src/lib/logout-admin-session.js +70 -0
- package/dist/src/lib/logout-admin-session.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +49 -0
- package/dist/src/lib/migration-nag.js +163 -0
- package/dist/src/lib/migration-nag.js.map +1 -0
- package/dist/src/lib/object-status-filter.d.ts +22 -0
- package/dist/src/lib/object-status-filter.js +45 -0
- package/dist/src/lib/object-status-filter.js.map +1 -0
- package/dist/src/lib/output.d.ts +124 -0
- package/dist/src/lib/output.js +219 -8
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/package-manager.d.ts +140 -0
- package/dist/src/lib/package-manager.js +305 -0
- package/dist/src/lib/package-manager.js.map +1 -0
- package/dist/src/lib/paginate.d.ts +98 -0
- package/dist/src/lib/paginate.js +112 -0
- package/dist/src/lib/paginate.js.map +1 -0
- package/dist/src/lib/platform-owned.d.ts +63 -0
- package/dist/src/lib/platform-owned.js +85 -0
- package/dist/src/lib/platform-owned.js.map +1 -0
- package/dist/src/lib/project-config.d.ts +122 -0
- package/dist/src/lib/project-config.js +244 -0
- package/dist/src/lib/project-config.js.map +1 -0
- package/dist/src/lib/prompt-cost-format.d.ts +11 -0
- package/dist/src/lib/prompt-cost-format.js +41 -0
- package/dist/src/lib/prompt-cost-format.js.map +1 -0
- package/dist/src/lib/prompt-schema-codegen.d.ts +147 -0
- package/dist/src/lib/prompt-schema-codegen.js +462 -0
- package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
- package/dist/src/lib/query-operators.d.ts +43 -0
- package/dist/src/lib/query-operators.js +80 -0
- package/dist/src/lib/query-operators.js.map +1 -0
- package/dist/src/lib/record-filter.d.ts +18 -0
- package/dist/src/lib/record-filter.js +55 -0
- package/dist/src/lib/record-filter.js.map +1 -0
- package/dist/src/lib/refresh-admin-credentials.d.ts +73 -0
- package/dist/src/lib/refresh-admin-credentials.js +123 -0
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
- package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
- package/dist/src/lib/resolve-init-dev-port.js +55 -0
- package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
- package/dist/src/lib/resolve-init-server.d.ts +64 -0
- package/dist/src/lib/resolve-init-server.js +77 -0
- package/dist/src/lib/resolve-init-server.js.map +1 -0
- package/dist/src/lib/resolve-owner.d.ts +19 -0
- package/dist/src/lib/resolve-owner.js +20 -0
- package/dist/src/lib/resolve-owner.js.map +1 -0
- package/dist/src/lib/resolve-platform.d.ts +74 -0
- package/dist/src/lib/resolve-platform.js +105 -0
- package/dist/src/lib/resolve-platform.js.map +1 -0
- package/dist/src/lib/run-status.d.ts +19 -0
- package/dist/src/lib/run-status.generated.d.ts +39 -0
- package/dist/src/lib/run-status.generated.js +66 -0
- package/dist/src/lib/run-status.generated.js.map +1 -0
- package/dist/src/lib/run-status.js +19 -0
- package/dist/src/lib/run-status.js.map +1 -0
- package/dist/src/lib/server-text-normalization.d.ts +51 -0
- package/dist/src/lib/server-text-normalization.js +90 -0
- package/dist/src/lib/server-text-normalization.js.map +1 -0
- package/dist/src/lib/server-url.d.ts +22 -0
- package/dist/src/lib/server-url.js +33 -0
- package/dist/src/lib/server-url.js.map +1 -0
- package/dist/src/lib/signing-secret-status.d.ts +81 -0
- package/dist/src/lib/signing-secret-status.js +116 -0
- package/dist/src/lib/signing-secret-status.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +71 -0
- package/dist/src/lib/skill-installer.js +441 -0
- package/dist/src/lib/skill-installer.js.map +1 -0
- package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
- package/dist/src/lib/snapshot-audit-source.js +58 -0
- package/dist/src/lib/snapshot-audit-source.js.map +1 -0
- package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
- package/dist/src/lib/snapshot-audit-store.js +196 -0
- package/dist/src/lib/snapshot-audit-store.js.map +1 -0
- package/dist/src/lib/snapshot-audit.d.ts +207 -0
- package/dist/src/lib/snapshot-audit.js +431 -0
- package/dist/src/lib/snapshot-audit.js.map +1 -0
- package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
- package/dist/src/lib/snapshot-build-rows.js +87 -0
- package/dist/src/lib/snapshot-build-rows.js.map +1 -0
- package/dist/src/lib/snapshot-build.d.ts +50 -0
- package/dist/src/lib/snapshot-build.js +111 -0
- package/dist/src/lib/snapshot-build.js.map +1 -0
- package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
- package/dist/src/lib/snapshot-manifest-layout.js +70 -0
- package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
- package/dist/src/lib/snapshots.d.ts +98 -0
- package/dist/src/lib/snapshots.js +294 -0
- package/dist/src/lib/snapshots.js.map +1 -0
- package/dist/src/lib/step-run-table.d.ts +43 -0
- package/dist/src/lib/step-run-table.js +129 -0
- package/dist/src/lib/step-run-table.js.map +1 -0
- package/dist/src/lib/storage-pending-retry.d.ts +26 -0
- package/dist/src/lib/storage-pending-retry.js +42 -0
- package/dist/src/lib/storage-pending-retry.js.map +1 -0
- package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/banners.d.ts +24 -0
- package/dist/src/lib/swift-codegen/banners.js +25 -0
- package/dist/src/lib/swift-codegen/banners.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +113 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +926 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +139 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js +462 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/generator.d.ts +100 -0
- package/dist/src/lib/swift-codegen/generator.js +457 -0
- package/dist/src/lib/swift-codegen/generator.js.map +1 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +87 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js +661 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +94 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js +155 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -0
- package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
- package/dist/src/lib/sync-dir-selector.d.ts +21 -0
- package/dist/src/lib/sync-dir-selector.js +30 -0
- package/dist/src/lib/sync-dir-selector.js.map +1 -0
- package/dist/src/lib/sync-paths.d.ts +128 -0
- package/dist/src/lib/sync-paths.js +195 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/sync-resource-types.d.ts +563 -0
- package/dist/src/lib/sync-resource-types.js +1073 -0
- package/dist/src/lib/sync-resource-types.js.map +1 -0
- package/dist/src/lib/sync-selectors.d.ts +138 -0
- package/dist/src/lib/sync-selectors.js +289 -0
- package/dist/src/lib/sync-selectors.js.map +1 -0
- package/dist/src/lib/template.d.ts +170 -0
- package/dist/src/lib/template.js +484 -68
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/test-case-file-names.d.ts +40 -0
- package/dist/src/lib/test-case-file-names.js +91 -0
- package/dist/src/lib/test-case-file-names.js.map +1 -0
- package/dist/src/lib/test-case-keys.d.ts +29 -0
- package/dist/src/lib/test-case-keys.js +55 -0
- package/dist/src/lib/test-case-keys.js.map +1 -0
- package/dist/src/lib/test-case-variables.d.ts +29 -0
- package/dist/src/lib/test-case-variables.js +71 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/token-inject.d.ts +56 -0
- package/dist/src/lib/token-inject.js +204 -0
- package/dist/src/lib/token-inject.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +123 -0
- package/dist/src/lib/toml-database-config.js +544 -0
- package/dist/src/lib/toml-database-config.js.map +1 -0
- package/dist/src/lib/toml-metadata-config.d.ts +151 -0
- package/dist/src/lib/toml-metadata-config.js +476 -0
- package/dist/src/lib/toml-metadata-config.js.map +1 -0
- package/dist/src/lib/toml-native-form.d.ts +46 -0
- package/dist/src/lib/toml-native-form.js +78 -0
- package/dist/src/lib/toml-native-form.js.map +1 -0
- package/dist/src/lib/toml-params-validator.d.ts +129 -0
- package/dist/src/lib/toml-params-validator.js +298 -0
- package/dist/src/lib/toml-params-validator.js.map +1 -0
- package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
- package/dist/src/lib/toml-scalar-edit.js +283 -0
- package/dist/src/lib/toml-scalar-edit.js.map +1 -0
- package/dist/src/lib/user-selector.d.ts +24 -0
- package/dist/src/lib/user-selector.js +33 -0
- package/dist/src/lib/user-selector.js.map +1 -0
- package/dist/src/lib/version-check.d.ts +35 -0
- package/dist/src/lib/version-check.js +241 -0
- package/dist/src/lib/version-check.js.map +1 -0
- package/dist/src/lib/watch.d.ts +121 -0
- package/dist/src/lib/watch.js +169 -0
- package/dist/src/lib/watch.js.map +1 -0
- package/dist/src/lib/web-url.d.ts +40 -0
- package/dist/src/lib/web-url.js +76 -0
- package/dist/src/lib/web-url.js.map +1 -0
- package/dist/src/lib/webhook-deliver.d.ts +209 -0
- package/dist/src/lib/webhook-deliver.js +519 -0
- package/dist/src/lib/webhook-deliver.js.map +1 -0
- package/dist/src/lib/workflow-apply.d.ts +110 -0
- package/dist/src/lib/workflow-apply.js +164 -0
- package/dist/src/lib/workflow-apply.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +129 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +269 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generator.d.ts +96 -0
- package/dist/src/lib/workflow-codegen/generator.js +361 -0
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +94 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +76 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
- package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
- package/dist/src/lib/workflow-codegen/naming.js +81 -0
- package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +80 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js +303 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
- package/dist/src/lib/workflow-config-apply.d.ts +70 -0
- package/dist/src/lib/workflow-config-apply.js +137 -0
- package/dist/src/lib/workflow-config-apply.js.map +1 -0
- package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
- package/dist/src/lib/workflow-config-sidecar.js +96 -0
- package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
- package/dist/src/lib/workflow-defaults.d.ts +29 -0
- package/dist/src/lib/workflow-defaults.js +41 -0
- package/dist/src/lib/workflow-defaults.js.map +1 -0
- package/dist/src/lib/workflow-fragments.d.ts +64 -0
- package/dist/src/lib/workflow-fragments.js +342 -0
- package/dist/src/lib/workflow-fragments.js.map +1 -0
- package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
- package/dist/src/lib/workflow-include-preserve.js +286 -0
- package/dist/src/lib/workflow-include-preserve.js.map +1 -0
- package/dist/src/lib/workflow-payload.d.ts +98 -0
- package/dist/src/lib/workflow-payload.js +178 -0
- package/dist/src/lib/workflow-payload.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.d.ts +211 -0
- package/dist/src/lib/workflow-toml-validator.js +770 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- package/dist/src/lib/workflow-usage.d.ts +196 -0
- package/dist/src/lib/workflow-usage.js +310 -0
- package/dist/src/lib/workflow-usage.js.map +1 -0
- package/dist/src/types/index.d.ts +591 -0
- package/dist/src/validators.d.ts +65 -0
- package/dist/src/validators.js +64 -0
- package/dist/src/validators.js.map +1 -0
- package/package.json +34 -9
|
@@ -1,21 +1,247 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync,
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
1
|
+
import { readFileSync, writeFileSync, readdirSync, } from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { stringifyConfigToml } from "../lib/config-toml.js";
|
|
4
|
+
import { generateWorkflowTypes, } from "../lib/workflow-codegen/generator.js";
|
|
5
|
+
import { generateWorkflowSwiftTypes } from "../lib/swift-codegen/generator.js";
|
|
5
6
|
import { ApiClient } from "../lib/api-client.js";
|
|
6
7
|
import { resolveAppId } from "../lib/config.js";
|
|
8
|
+
import { resolveWorkflowSelector } from "../lib/block-selector.js";
|
|
9
|
+
import { formatAccessRuleForDisplay } from "../lib/access-rule-display.js";
|
|
10
|
+
import { resolveCodegenSourceDir } from "../lib/codegen-shared/resolveCodegenSourceDir.js";
|
|
11
|
+
import { expandWorkflow } from "../lib/workflow-fragments.js";
|
|
12
|
+
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
13
|
+
import { parseStatusFilter } from "../lib/object-status-filter.js";
|
|
14
|
+
import { buildWorkflowRunEnvelope, buildWorkflowRunListEnvelope, buildWorkflowStepEnvelope, collectWorkflowFailures, describeEmptyRunPage, describeFailureSweep, formatFailureCodeCell, formatFailureErrorCell, formatFailureStepCell, selectFailedStepRow, toWorkflowRunInspectionRow, DEFAULT_FAILURE_SWEEP_MAX_SCAN, FAILURE_SWEEP_MAX_REQUESTS, SERVER_SCAN_ROWS_PER_REQUEST, } from "../lib/log-inspection.js";
|
|
7
15
|
import chalk from "chalk";
|
|
8
|
-
import { success, error, info, warn, keyValue, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, } from "../lib/output.js";
|
|
16
|
+
import { success, error, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, formatStatus, formatDuration, json, jsonLine, divider, dim, progress, progressEnd, } from "../lib/output.js";
|
|
17
|
+
import { pollUntil, followLoop, watchLoop, parseIntervalSeconds, } from "../lib/watch.js";
|
|
18
|
+
import { APPLY_RUN_STATUSES, RUN_STATUS_LIST, SETTLED_RUN_STATUSES, } from "../lib/run-status.js";
|
|
19
|
+
import { requireTestCaseInputVariables } from "../lib/test-case-variables.js";
|
|
20
|
+
/**
|
|
21
|
+
* Render the `Step Results` section of `workflows runs status`. Pure
|
|
22
|
+
* formatting helper — returns lines as strings so it's unit-testable
|
|
23
|
+
* (the caller console.logs them). For each step, emits:
|
|
24
|
+
*
|
|
25
|
+
* ` <id>: <status-label> <duration>`
|
|
26
|
+
*
|
|
27
|
+
* and, when applicable, indented follow-up lines:
|
|
28
|
+
*
|
|
29
|
+
* - `error_captured` step with `error` → ` error: <truncated message>`
|
|
30
|
+
* - forEach output with `errors[]` non-empty →
|
|
31
|
+
* ` forEach errors: <N/M> - first: <truncated message>`
|
|
32
|
+
*
|
|
33
|
+
* The truncation length matches `runs steps` ERROR column (80 chars,
|
|
34
|
+
* trailing "..." when truncated). See #688.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Stop-polling predicate for the `workflows preview --wait` loop (issues #941,
|
|
38
|
+
* #2348). True when the run has settled and the wait should stop — as opposed
|
|
39
|
+
* to a still-in-flight status ("queued"/"running") that keeps the loop going
|
|
40
|
+
* until the cap.
|
|
41
|
+
*
|
|
42
|
+
* Since #2348 the server reconciles every status surface onto the canonical
|
|
43
|
+
* vocabulary before it goes on the wire, so this reads the one shared set in
|
|
44
|
+
* `lib/run-status.ts` instead of re-deriving terminality here:
|
|
45
|
+
* - "completed" / "failed" / "terminated" — true terminals. "terminated" is
|
|
46
|
+
* an aborted preview, a real end state (Codex P2 on #941: it used to be
|
|
47
|
+
* omitted, so an abort was misreported as a wait timeout).
|
|
48
|
+
* - "apply_pending" / "apply_claimed" — the run finished and is parked
|
|
49
|
+
* waiting for a client to apply its result. It never advances on its own,
|
|
50
|
+
* so the previous ladder spun here until the 60s cap.
|
|
51
|
+
* - "missing" — no live instance to observe; more polling cannot help.
|
|
52
|
+
*
|
|
53
|
+
* The raw Cloudflare spellings ("complete"/"errored"/"waiting") are gone: the
|
|
54
|
+
* server no longer puts them on the wire, and matching them here would have
|
|
55
|
+
* kept two vocabularies alive.
|
|
56
|
+
*/
|
|
57
|
+
export function isPreviewWaitTerminalStatus(s) {
|
|
58
|
+
return typeof s === "string" && SETTLED_RUN_STATUSES.has(s);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Which arm the `workflows preview --wait` render ladder takes (#2636).
|
|
62
|
+
*
|
|
63
|
+
* Extracted from the ladder so the branch decision is testable: the point of
|
|
64
|
+
* the `skipped` arm is that an elided preview — one whose declarative lock was
|
|
65
|
+
* held under `onContention: "ignore"` — reports that it did not run, instead
|
|
66
|
+
* of falling through to the timeout branch and exiting 124 as though it were
|
|
67
|
+
* still going.
|
|
68
|
+
*/
|
|
69
|
+
export function previewWaitOutcome(status) {
|
|
70
|
+
if (status === "completed")
|
|
71
|
+
return "completed";
|
|
72
|
+
if (status === "failed")
|
|
73
|
+
return "failed";
|
|
74
|
+
if (status === "terminated")
|
|
75
|
+
return "terminated";
|
|
76
|
+
if (status === "skipped")
|
|
77
|
+
return "skipped";
|
|
78
|
+
if (typeof status === "string" && APPLY_RUN_STATUSES.has(status))
|
|
79
|
+
return "apply";
|
|
80
|
+
if (status === "missing")
|
|
81
|
+
return "missing";
|
|
82
|
+
return "timeout";
|
|
83
|
+
}
|
|
84
|
+
export function renderRunStatusStepResults(stepResults) {
|
|
85
|
+
const lines = [];
|
|
86
|
+
for (const step of stepResults || []) {
|
|
87
|
+
const statusLabel = step.skipped
|
|
88
|
+
? chalk.gray("skipped")
|
|
89
|
+
: step.status === "error_captured"
|
|
90
|
+
? chalk.yellow("error_captured")
|
|
91
|
+
: step.status === "failed"
|
|
92
|
+
? chalk.red("failed")
|
|
93
|
+
: step.status === "completed"
|
|
94
|
+
? chalk.green("completed")
|
|
95
|
+
: step.status || "?";
|
|
96
|
+
const durationLabel = formatDuration(step.durationMs);
|
|
97
|
+
lines.push(` ${step.id}: ${statusLabel} ${durationLabel}`);
|
|
98
|
+
// Single-step error_captured: show the captured error message.
|
|
99
|
+
if (step.status === "error_captured" && step.error) {
|
|
100
|
+
const errMsg = typeof step.error === "string" ? step.error : String(step.error);
|
|
101
|
+
const truncated = errMsg.length > 80 ? errMsg.slice(0, 77) + "..." : errMsg;
|
|
102
|
+
lines.push(` ${chalk.dim("error:")} ${chalk.yellow(truncated)}`);
|
|
103
|
+
}
|
|
104
|
+
else if (step.status === "error_captured" && step.errorDetails) {
|
|
105
|
+
// No top-level error but errorDetails present — hint to drill in.
|
|
106
|
+
lines.push(` ${chalk.dim("error: <see runs step-detail for full errorDetails>")}`);
|
|
107
|
+
}
|
|
108
|
+
// forEach aggregated errors: a one-line summary surfaces the count
|
|
109
|
+
// of `error_captured` iterations and a sample message (#688).
|
|
110
|
+
const fo = step.output;
|
|
111
|
+
if (fo && typeof fo === "object" && Array.isArray(fo.errors) && fo.errors.length > 0) {
|
|
112
|
+
const total = (Array.isArray(fo.items) ? fo.items.length : 0)
|
|
113
|
+
|| (fo.totalSucceeded ?? 0) + (fo.totalFailed ?? 0);
|
|
114
|
+
const firstErr = (Array.isArray(fo.errorMessages) && fo.errorMessages.length > 0
|
|
115
|
+
? fo.errorMessages[0]
|
|
116
|
+
: (fo.errors[0]?.error ?? "?"));
|
|
117
|
+
const firstStr = typeof firstErr === "string" ? firstErr : String(firstErr);
|
|
118
|
+
const truncated = firstStr.length > 80 ? firstStr.slice(0, 77) + "..." : firstStr;
|
|
119
|
+
lines.push(` ${chalk.dim("forEach errors:")} ${chalk.yellow(`${fo.totalFailed ?? fo.errors.length}/${total}`)} ${chalk.dim("- first:")} ${chalk.yellow(truncated)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
// #3348 — the step table, the gap annotation and the running-step duration now
|
|
125
|
+
// live in `lib/step-run-table.ts`, shared with `functions runs steps`: a
|
|
126
|
+
// function run's steps are the same rows, and the two views must not be two
|
|
127
|
+
// tables that merely resemble each other. Re-exported from here because this is
|
|
128
|
+
// where the two helpers were first exported for testability.
|
|
129
|
+
export { annotateStepGaps, stepRunDisplayDurationMs, } from "../lib/step-run-table.js";
|
|
130
|
+
import { renderStepRunTable, stepRunDisplayDurationMs, } from "../lib/step-run-table.js";
|
|
131
|
+
import { normalizeCliListEnvelope, wholeListEnvelope, } from "../lib/paginate.js";
|
|
132
|
+
import { pageCursorOption, pageLimitOption, parsePageLimit, printEmptyPage, printPageHint, } from "../lib/list-options.js";
|
|
133
|
+
/**
|
|
134
|
+
* #2771 — the step lines `workflows runs status` should render.
|
|
135
|
+
*
|
|
136
|
+
* Cloudflare's instance status carries `stepResults` only once the run reaches
|
|
137
|
+
* a terminal state, so a live run used to print `Status: running` and nothing
|
|
138
|
+
* else. The persisted step rows now exist DURING the run, so they are the
|
|
139
|
+
* fallback: mapped onto the shape `renderRunStatusStepResults` reads, with a
|
|
140
|
+
* running step's elapsed time in place of its 0 placeholder. The instance's own
|
|
141
|
+
* results always win when it has them. Exported for testability.
|
|
142
|
+
*/
|
|
143
|
+
export function stepResultsForRunStatus(instanceStepResults, persistedStepRuns, now = Date.now()) {
|
|
144
|
+
if (instanceStepResults && instanceStepResults.length > 0)
|
|
145
|
+
return instanceStepResults;
|
|
146
|
+
return (persistedStepRuns || []).map((row) => ({
|
|
147
|
+
id: row?.stepId,
|
|
148
|
+
kind: row?.stepKind,
|
|
149
|
+
status: row?.status,
|
|
150
|
+
durationMs: stepRunDisplayDurationMs(row, now),
|
|
151
|
+
error: row?.error,
|
|
152
|
+
errorDetails: row?.errorDetails,
|
|
153
|
+
output: row?.output,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* #1367 — compute min / p50 / p95 / max over a set of latency samples (ms).
|
|
158
|
+
* Ignores non-finite values. Returns `null` when there are no valid samples.
|
|
159
|
+
* Percentiles use nearest-rank on the sorted samples. Exported for testability.
|
|
160
|
+
*/
|
|
161
|
+
export function summarizeLatencyDistribution(values) {
|
|
162
|
+
const nums = (values || []).filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
163
|
+
if (nums.length === 0)
|
|
164
|
+
return null;
|
|
165
|
+
nums.sort((a, b) => a - b);
|
|
166
|
+
const pct = (p) => {
|
|
167
|
+
const rank = Math.ceil((p / 100) * nums.length);
|
|
168
|
+
return nums[Math.min(nums.length - 1, Math.max(0, rank - 1))];
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
count: nums.length,
|
|
172
|
+
min: nums[0],
|
|
173
|
+
p50: pct(50),
|
|
174
|
+
p95: pct(95),
|
|
175
|
+
max: nums[nums.length - 1],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Issue #687 (review feedback): render an unambiguous label for a workflow
|
|
180
|
+
* config slot ("active config", "draft config", etc.) when the slot may be
|
|
181
|
+
* unnamed. Falls back to a short ID prefix so users staging multiple unnamed
|
|
182
|
+
* configs can still tell them apart.
|
|
183
|
+
*
|
|
184
|
+
* formatConfigSlotLabel("active config", "v2", "01KRT...") → 'active config "v2"'
|
|
185
|
+
* formatConfigSlotLabel("active config", null, "01KRTABCDXYZ...") → 'active config (01KRTABCDXYZ…)'
|
|
186
|
+
* formatConfigSlotLabel("active config", null, null) → 'active config'
|
|
187
|
+
*
|
|
188
|
+
* Exported for testability.
|
|
189
|
+
*/
|
|
190
|
+
export function formatConfigSlotLabel(slot, configName, configId, prefixLength = 12) {
|
|
191
|
+
if (configName) {
|
|
192
|
+
return `${slot} "${configName}"`;
|
|
193
|
+
}
|
|
194
|
+
if (configId) {
|
|
195
|
+
return `${slot} (${String(configId).slice(0, prefixLength)}…)`;
|
|
196
|
+
}
|
|
197
|
+
return slot;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Resolve a workflow reference — its server id, or the author-chosen key its
|
|
201
|
+
* TOML file is named for — for the `tests` verbs (#3249).
|
|
202
|
+
*
|
|
203
|
+
* The key is what an operator has: `workflows/<key>.toml` is the object and
|
|
204
|
+
* `workflows/<key>.tests/` its cases. The resolution lives in
|
|
205
|
+
* `block-selector.ts`, shared with `prompts`.
|
|
206
|
+
*/
|
|
207
|
+
async function resolveWorkflowId(client, appId, ref) {
|
|
208
|
+
return (await resolveWorkflowSelector(client, appId, ref)).blockId;
|
|
209
|
+
}
|
|
9
210
|
export function registerWorkflowsCommands(program) {
|
|
10
211
|
const workflows = program
|
|
11
212
|
.command("workflows")
|
|
12
|
-
.description("
|
|
213
|
+
.description("Inspect workflows, run and monitor them, and take one out of service")
|
|
13
214
|
.addHelpText("after", `
|
|
14
215
|
Examples:
|
|
15
216
|
$ primitive workflows list
|
|
16
|
-
$ primitive
|
|
17
|
-
$ primitive
|
|
217
|
+
$ primitive config create workflow process-doc # scaffold workflows/process-doc.toml
|
|
218
|
+
$ primitive config push --only workflow/process-doc # apply it
|
|
18
219
|
$ primitive workflows runs list 01HXY...
|
|
220
|
+
$ primitive workflows disable 01HXY... # take it out of service now
|
|
221
|
+
`);
|
|
222
|
+
// Server-side configuration is authored in TOML and applied with `config push`
|
|
223
|
+
// (issue #2645) — this group no longer carries config-setting flags.
|
|
224
|
+
workflows.addHelpText("after", `
|
|
225
|
+
Configuration lives in workflows/<key>.toml:
|
|
226
|
+
$ primitive config fields workflow # the file's keys, types and defaults
|
|
227
|
+
$ primitive config create workflow <key> # scaffold one locally
|
|
228
|
+
$ primitive config set workflow/<key> <path>=<value>
|
|
229
|
+
$ primitive config push --only workflow/<key>
|
|
230
|
+
|
|
231
|
+
Which body runs is configuration too: name it with activeConfigName in
|
|
232
|
+
workflows/<key>.toml (bodies live in workflows/<key>.configs/<name>.toml) and
|
|
233
|
+
apply it with 'config push'. There is no other write path: revisions are
|
|
234
|
+
read-only history from the retired legacy path (issue #2768).
|
|
235
|
+
`);
|
|
236
|
+
// Per-subject analytics have one home, the `analytics` noun (issue #2766) —
|
|
237
|
+
// this group's own analytics commands were removed, so say where they went.
|
|
238
|
+
workflows.addHelpText("after", `
|
|
239
|
+
Analytics live under the analytics noun:
|
|
240
|
+
$ primitive analytics workflows --window-days 7 --limit 10
|
|
241
|
+
|
|
242
|
+
That is the single home for per-subject analytics (workflows, prompts,
|
|
243
|
+
integrations). It ranks workflows by runs; there is no app-wide workflow
|
|
244
|
+
overview — the endpoint behind the retired overview command never existed.
|
|
19
245
|
`);
|
|
20
246
|
// List workflows
|
|
21
247
|
workflows
|
|
@@ -23,21 +249,35 @@ Examples:
|
|
|
23
249
|
.description("List workflows")
|
|
24
250
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
25
251
|
.option("--app <app-id>", "App ID")
|
|
26
|
-
.option("--status <status>", "Filter by status:
|
|
252
|
+
.option("--status <status>", "Filter by status: active, inactive, archived")
|
|
253
|
+
.addOption(pageLimitOption())
|
|
254
|
+
.addOption(pageCursorOption())
|
|
27
255
|
.option("--json", "Output as JSON")
|
|
28
256
|
.action(async (appId, options) => {
|
|
257
|
+
const limit = parsePageLimit(options.limit);
|
|
29
258
|
const resolvedAppId = resolveAppId(appId, options);
|
|
30
259
|
const client = new ApiClient();
|
|
31
260
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
261
|
+
// #2803 — `draft` is gone; refuse it by name rather than returning the
|
|
262
|
+
// empty list a canonical filter would produce. #2887 added `archived`:
|
|
263
|
+
// the delete flow writes one now, so asking for the retired rows is a
|
|
264
|
+
// question with an answer rather than a 400.
|
|
265
|
+
const page = normalizeCliListEnvelope(await client.listWorkflows(resolvedAppId, {
|
|
266
|
+
status: parseStatusFilter("workflows", options.status, [
|
|
267
|
+
"active",
|
|
268
|
+
"inactive",
|
|
269
|
+
"archived",
|
|
270
|
+
]),
|
|
271
|
+
limit,
|
|
272
|
+
cursor: options.cursor,
|
|
273
|
+
}));
|
|
274
|
+
const items = page.items;
|
|
35
275
|
if (options.json) {
|
|
36
|
-
json(
|
|
276
|
+
json(page);
|
|
37
277
|
return;
|
|
38
278
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
279
|
+
if (items.length === 0) {
|
|
280
|
+
printEmptyPage(page, "workflows");
|
|
41
281
|
return;
|
|
42
282
|
}
|
|
43
283
|
console.log(formatTable(items, [
|
|
@@ -47,89 +287,17 @@ Examples:
|
|
|
47
287
|
{ header: "STATUS", key: "status", format: formatStatus },
|
|
48
288
|
{ header: "MODIFIED", key: "modifiedAt", format: formatDate },
|
|
49
289
|
]));
|
|
290
|
+
printPageHint(page);
|
|
50
291
|
}
|
|
51
292
|
catch (err) {
|
|
52
293
|
error(err.message);
|
|
53
294
|
process.exit(1);
|
|
54
295
|
}
|
|
55
296
|
});
|
|
56
|
-
// Create workflow
|
|
57
|
-
workflows
|
|
58
|
-
.command("create")
|
|
59
|
-
.description("Create a new workflow")
|
|
60
|
-
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
61
|
-
.option("--app <app-id>", "App ID")
|
|
62
|
-
.option("--key <key>", "Workflow key (unique identifier)")
|
|
63
|
-
.option("--name <name>", "Display name")
|
|
64
|
-
.option("--description <desc>", "Description")
|
|
65
|
-
.option("--from-file <path>", "Load workflow from TOML file")
|
|
66
|
-
.option("--requires-client-apply <bool>", "Require client-side apply: true or false")
|
|
67
|
-
.option("--json", "Output as JSON")
|
|
68
|
-
.action(async (appId, options) => {
|
|
69
|
-
const resolvedAppId = resolveAppId(appId, options);
|
|
70
|
-
const client = new ApiClient();
|
|
71
|
-
let payload;
|
|
72
|
-
if (options.fromFile) {
|
|
73
|
-
try {
|
|
74
|
-
const content = readFileSync(options.fromFile, "utf-8");
|
|
75
|
-
const tomlData = TOML.parse(content);
|
|
76
|
-
const workflow = tomlData.workflow || tomlData;
|
|
77
|
-
payload = {
|
|
78
|
-
workflowKey: workflow.key || workflow.workflowKey,
|
|
79
|
-
name: workflow.name,
|
|
80
|
-
description: workflow.description,
|
|
81
|
-
steps: tomlData.steps || [],
|
|
82
|
-
inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
|
|
83
|
-
outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
|
|
84
|
-
perUserMaxRunning: workflow.perUserMaxRunning,
|
|
85
|
-
perUserMaxQueued: workflow.perUserMaxQueued,
|
|
86
|
-
perAppMaxRunning: workflow.perAppMaxRunning,
|
|
87
|
-
perAppMaxQueued: workflow.perAppMaxQueued,
|
|
88
|
-
queueTtlSeconds: workflow.queueTtlSeconds,
|
|
89
|
-
dequeueOrder: workflow.dequeueOrder,
|
|
90
|
-
requiresClientApply: workflow.requiresClientApply,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
catch (err) {
|
|
94
|
-
error(`Failed to read TOML file: ${err.message}`);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
if (!options.key || !options.name) {
|
|
100
|
-
error("Required: --key, --name (or use --from-file)");
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
payload = {
|
|
104
|
-
workflowKey: options.key,
|
|
105
|
-
name: options.name,
|
|
106
|
-
description: options.description,
|
|
107
|
-
steps: [],
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
if (options.requiresClientApply !== undefined) {
|
|
111
|
-
payload.requiresClientApply = options.requiresClientApply === "true";
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
const result = await client.createWorkflow(resolvedAppId, payload);
|
|
115
|
-
if (options.json) {
|
|
116
|
-
json(result);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
success(`Workflow created: ${result.workflow?.name || options.name}`);
|
|
120
|
-
keyValue("Workflow ID", result.workflow?.workflowId);
|
|
121
|
-
keyValue("Key", result.workflow?.workflowKey);
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
error(err.message);
|
|
125
|
-
process.exit(1);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
// Get workflow
|
|
129
297
|
workflows
|
|
130
298
|
.command("get")
|
|
131
299
|
.description("Get workflow details")
|
|
132
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
300
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
133
301
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
134
302
|
.option("--json", "Output as JSON")
|
|
135
303
|
.action(async (workflowId, options) => {
|
|
@@ -142,20 +310,36 @@ Examples:
|
|
|
142
310
|
return;
|
|
143
311
|
}
|
|
144
312
|
const wf = result.workflow;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
313
|
+
printResult("Workflow ID", wf.workflowId);
|
|
314
|
+
printResult("Key", wf.workflowKey);
|
|
315
|
+
printResult("Name", wf.name);
|
|
316
|
+
printResult("Description", wf.description);
|
|
317
|
+
// #2803 — ONE availability row. The server returns the canonical
|
|
318
|
+
// value, so a workflow an operator disabled reads `inactive` here and
|
|
319
|
+
// there is no second row left to disagree with it.
|
|
320
|
+
printResult("Status", formatStatus(wf.status));
|
|
321
|
+
printResult("Active Config", wf.activeConfigId || "-");
|
|
322
|
+
printResult("Latest Revision", wf.latestRevision || "-");
|
|
323
|
+
printResult("Client Apply", wf.requiresClientApply !== false ? "yes" : "no");
|
|
324
|
+
printResult("Sync Callable", wf.syncCallable === true ? "yes" : "no");
|
|
325
|
+
// #1233 — runAs unset means the engine defaults to "caller"; show that
|
|
326
|
+
// default rather than a blank so the deployed principal mode is visible.
|
|
327
|
+
printResult("Run As", wf.runAs || "caller");
|
|
328
|
+
printResult("Capabilities", Array.isArray(wf.capabilities) && wf.capabilities.length > 0
|
|
329
|
+
? wf.capabilities.join(", ")
|
|
330
|
+
: "-");
|
|
331
|
+
// #2652: a caller workflow with no rule denies every non-admin start,
|
|
332
|
+
// so the row is always printed — the sentinel names the consequence,
|
|
333
|
+
// and a `runAs: "system"` workflow gets its own exempt wording.
|
|
334
|
+
printResult("Access Rule", formatAccessRuleForDisplay(wf.accessRule, "workflow", {
|
|
335
|
+
runAs: wf.runAs,
|
|
336
|
+
}));
|
|
153
337
|
divider();
|
|
154
338
|
info("Queue Settings:");
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
339
|
+
printResult(" Per User Max Running", wf.perUserMaxRunning);
|
|
340
|
+
printResult(" Per User Max Queued", wf.perUserMaxQueued);
|
|
341
|
+
printResult(" Per App Max Running", wf.perAppMaxRunning);
|
|
342
|
+
printResult(" Dequeue Order", wf.dequeueOrder);
|
|
159
343
|
if (wf.inputSchema) {
|
|
160
344
|
divider();
|
|
161
345
|
info("Input Schema:");
|
|
@@ -196,163 +380,136 @@ Examples:
|
|
|
196
380
|
process.exit(1);
|
|
197
381
|
}
|
|
198
382
|
});
|
|
199
|
-
// Update workflow
|
|
200
383
|
workflows
|
|
201
|
-
.command("
|
|
202
|
-
.description("
|
|
203
|
-
.argument("<
|
|
204
|
-
.option("--
|
|
205
|
-
.
|
|
206
|
-
.option("--description <desc>", "Description")
|
|
207
|
-
.option("--status <status>", "Status: draft, active, archived")
|
|
208
|
-
.option("--per-user-max-running <n>", "Max running per user")
|
|
209
|
-
.option("--per-user-max-queued <n>", "Max queued per user")
|
|
210
|
-
.option("--dequeue-order <order>", "Dequeue order: fifo, lifo")
|
|
211
|
-
.option("--requires-client-apply <bool>", "Require client-side apply: true or false")
|
|
212
|
-
.option("--json", "Output as JSON")
|
|
213
|
-
.action(async (workflowId, options) => {
|
|
214
|
-
const resolvedAppId = resolveAppId(undefined, options);
|
|
215
|
-
const payload = {};
|
|
216
|
-
if (options.name)
|
|
217
|
-
payload.name = options.name;
|
|
218
|
-
if (options.description)
|
|
219
|
-
payload.description = options.description;
|
|
220
|
-
if (options.status)
|
|
221
|
-
payload.status = options.status;
|
|
222
|
-
if (options.perUserMaxRunning)
|
|
223
|
-
payload.perUserMaxRunning = parseInt(options.perUserMaxRunning);
|
|
224
|
-
if (options.perUserMaxQueued)
|
|
225
|
-
payload.perUserMaxQueued = parseInt(options.perUserMaxQueued);
|
|
226
|
-
if (options.dequeueOrder)
|
|
227
|
-
payload.dequeueOrder = options.dequeueOrder;
|
|
228
|
-
if (options.requiresClientApply !== undefined) {
|
|
229
|
-
payload.requiresClientApply = options.requiresClientApply === "true";
|
|
230
|
-
}
|
|
231
|
-
if (Object.keys(payload).length === 0) {
|
|
232
|
-
error("No update options specified.");
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
const client = new ApiClient();
|
|
384
|
+
.command("expand")
|
|
385
|
+
.description("Expand a workflow TOML's include fragments and print the result")
|
|
386
|
+
.argument("<file>", "Path to the workflow TOML file")
|
|
387
|
+
.option("--format <format>", "Output format: json (default) or toml", "json")
|
|
388
|
+
.action((file, options) => {
|
|
236
389
|
try {
|
|
237
|
-
const
|
|
238
|
-
if (options.
|
|
239
|
-
|
|
240
|
-
|
|
390
|
+
const expanded = expandWorkflow(file);
|
|
391
|
+
if (options.format === "toml") {
|
|
392
|
+
// Strip the `include` key (already removed by expander) and emit
|
|
393
|
+
// valid TOML so the result is round-trippable.
|
|
394
|
+
console.log(stringifyConfigToml(expanded));
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
console.log(JSON.stringify(expanded, null, 2));
|
|
241
398
|
}
|
|
242
|
-
success("Workflow updated.");
|
|
243
399
|
}
|
|
244
400
|
catch (err) {
|
|
245
|
-
error(err
|
|
401
|
+
error(err?.message ?? String(err));
|
|
246
402
|
process.exit(1);
|
|
247
403
|
}
|
|
248
404
|
});
|
|
249
|
-
//
|
|
405
|
+
// #2645 criterion 9 / #2803 — the operational verbs.
|
|
406
|
+
//
|
|
407
|
+
// Configuration is authored in TOML and applied with `config push`, which would
|
|
408
|
+
// otherwise mean an operator needs a repo checkout to stop a misbehaving
|
|
409
|
+
// workflow. It does not: taking something out of service is an OPERATION, and
|
|
410
|
+
// these two verbs are the ONLY writers of `status`, which is server-owned and
|
|
411
|
+
// no longer a TOML key. The committed configuration says nothing about
|
|
412
|
+
// availability, so `config diff` reports the server's status as operator
|
|
413
|
+
// state rather than as drift.
|
|
250
414
|
workflows
|
|
251
|
-
.command("
|
|
252
|
-
.description("
|
|
253
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
415
|
+
.command("disable")
|
|
416
|
+
.description("Take a workflow out of service now. Operational, not configuration: `status` is server-owned and 'config push' can neither set nor clear it.")
|
|
417
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
254
418
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
255
|
-
.option("--
|
|
256
|
-
.option("-y, --yes", "Skip confirmation prompt")
|
|
419
|
+
.option("--json", "Output as JSON")
|
|
257
420
|
.action(async (workflowId, options) => {
|
|
258
421
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
259
|
-
if (!options.yes) {
|
|
260
|
-
const action = options.hard ? "permanently delete" : "archive";
|
|
261
|
-
const inquirer = await import("inquirer");
|
|
262
|
-
const { confirm } = await inquirer.default.prompt([
|
|
263
|
-
{
|
|
264
|
-
type: "confirm",
|
|
265
|
-
name: "confirm",
|
|
266
|
-
message: `Are you sure you want to ${action} workflow ${workflowId}?`,
|
|
267
|
-
default: false,
|
|
268
|
-
},
|
|
269
|
-
]);
|
|
270
|
-
if (!confirm) {
|
|
271
|
-
info("Cancelled.");
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
422
|
const client = new ApiClient();
|
|
276
423
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
else {
|
|
282
|
-
await client.updateWorkflow(resolvedAppId, workflowId, { status: "archived" });
|
|
283
|
-
success("Workflow archived.");
|
|
424
|
+
const result = await client.disableWorkflow(resolvedAppId, workflowId);
|
|
425
|
+
if (options.json) {
|
|
426
|
+
json(result);
|
|
427
|
+
return;
|
|
284
428
|
}
|
|
429
|
+
success(`Workflow disabled: ${result.workflowKey || workflowId}`);
|
|
430
|
+
info("New runs are refused. Configuration is unchanged — `primitive workflows enable` puts it back.");
|
|
285
431
|
}
|
|
286
432
|
catch (err) {
|
|
287
433
|
error(err.message);
|
|
288
434
|
process.exit(1);
|
|
289
435
|
}
|
|
290
436
|
});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
.command("update")
|
|
296
|
-
.description("Update workflow draft steps")
|
|
297
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
437
|
+
workflows
|
|
438
|
+
.command("enable")
|
|
439
|
+
.description("Put a workflow back in service.")
|
|
440
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
298
441
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
299
|
-
.option("--from-file <path>", "Load steps from TOML file")
|
|
300
442
|
.option("--json", "Output as JSON")
|
|
301
443
|
.action(async (workflowId, options) => {
|
|
302
444
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
303
|
-
if (!options.fromFile) {
|
|
304
|
-
error("--from-file is required");
|
|
305
|
-
process.exit(1);
|
|
306
|
-
}
|
|
307
|
-
let payload;
|
|
308
|
-
try {
|
|
309
|
-
const content = readFileSync(options.fromFile, "utf-8");
|
|
310
|
-
const tomlData = TOML.parse(content);
|
|
311
|
-
const workflow = tomlData.workflow || tomlData;
|
|
312
|
-
payload = {
|
|
313
|
-
steps: tomlData.steps || [],
|
|
314
|
-
inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
|
|
315
|
-
outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
catch (err) {
|
|
319
|
-
error(`Failed to read TOML file: ${err.message}`);
|
|
320
|
-
process.exit(1);
|
|
321
|
-
}
|
|
322
445
|
const client = new ApiClient();
|
|
323
446
|
try {
|
|
324
|
-
const result = await client.
|
|
447
|
+
const result = await client.enableWorkflow(resolvedAppId, workflowId);
|
|
325
448
|
if (options.json) {
|
|
326
449
|
json(result);
|
|
327
450
|
return;
|
|
328
451
|
}
|
|
329
|
-
success(
|
|
330
|
-
keyValue("Steps", result.draft?.steps?.length || 0);
|
|
452
|
+
success(`Workflow enabled: ${result.workflowKey || workflowId}`);
|
|
331
453
|
}
|
|
332
454
|
catch (err) {
|
|
333
455
|
error(err.message);
|
|
334
456
|
process.exit(1);
|
|
335
457
|
}
|
|
336
458
|
});
|
|
337
|
-
//
|
|
459
|
+
// #2907 — the delete flow's CLI spelling. Not a second writer of `status`:
|
|
460
|
+
// this is the plain `DELETE` #2887 made retire-with-history, which until now
|
|
461
|
+
// only the admin API and the console could reach. Named `archive`, not
|
|
462
|
+
// `delete`, because a configuration noun gets no `delete` verb
|
|
463
|
+
// (docs/cli-design.md) and prune-by-push stays the only CLI hard-delete path.
|
|
338
464
|
workflows
|
|
339
|
-
.command("
|
|
340
|
-
.description("
|
|
341
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
465
|
+
.command("archive")
|
|
466
|
+
.description("Retire a workflow (soft delete). Delete-lifecycle, not `disable`: the definition is kept so its history still resolves, it goes on holding its key, and there is no un-archive.")
|
|
467
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
342
468
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
469
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
343
470
|
.option("--json", "Output as JSON")
|
|
471
|
+
.addHelpText("after", `
|
|
472
|
+
What archiving a workflow costs (retire-with-history):
|
|
473
|
+
New runs are refused, and so is every \`workflow.call\` into it.
|
|
474
|
+
Its runs, revisions, configurations and test cases keep resolving what they
|
|
475
|
+
point at — that is what the tombstone exists for.
|
|
476
|
+
The archived row still holds its workflowKey.
|
|
477
|
+
\`primitive workflows enable\` refuses an archived workflow; there is no
|
|
478
|
+
un-archive.
|
|
479
|
+
|
|
480
|
+
Reclaiming the key means hard-deleting the row and its cascade:
|
|
481
|
+
$ rm workflows/<key>.toml
|
|
482
|
+
$ primitive config push --prune # confirmed; hard-deletes the row
|
|
483
|
+
# or hard-delete it in the console, then re-add the file and push.
|
|
484
|
+
`)
|
|
344
485
|
.action(async (workflowId, options) => {
|
|
345
486
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
487
|
+
if (!options.yes) {
|
|
488
|
+
let confirmed;
|
|
489
|
+
try {
|
|
490
|
+
confirmed = await confirmPrompt(`Archive workflow ${workflowId}? New runs are refused, its history is ` +
|
|
491
|
+
`kept, it goes on holding its key, and there is no un-archive.`);
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
error(err.message);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
if (!confirmed) {
|
|
498
|
+
info("Cancelled.");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
346
502
|
const client = new ApiClient();
|
|
347
503
|
try {
|
|
348
|
-
|
|
504
|
+
// The soft delete: no `hard`, so the #1238 cascade does not run and the
|
|
505
|
+
// definition row stays queryable.
|
|
506
|
+
const result = await client.deleteWorkflow(resolvedAppId, workflowId);
|
|
349
507
|
if (options.json) {
|
|
350
508
|
json(result);
|
|
351
509
|
return;
|
|
352
510
|
}
|
|
353
|
-
success(
|
|
354
|
-
|
|
355
|
-
keyValue("Published At", formatDate(result.revision?.publishedAt));
|
|
511
|
+
success(`Workflow archived: ${workflowId}`);
|
|
512
|
+
info("Runs, revisions and test cases keep resolving. The row still holds its key — reclaim it by removing its file and running a confirmed `primitive config push --prune`.");
|
|
356
513
|
}
|
|
357
514
|
catch (err) {
|
|
358
515
|
error(err.message);
|
|
@@ -363,14 +520,20 @@ Examples:
|
|
|
363
520
|
workflows
|
|
364
521
|
.command("preview")
|
|
365
522
|
.description("Run a preview execution of the workflow")
|
|
366
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
523
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
367
524
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
368
|
-
.option("--config <config-id>", "Use specific configuration
|
|
525
|
+
.option("--config <config-id>", "Use specific configuration")
|
|
526
|
+
.option("--draft", "Force preview of the draft version, even if active is newer")
|
|
527
|
+
.option("--active", "Force preview of the active config, even if a newer draft exists (issue #687)")
|
|
369
528
|
.option("--input <json>", "Root input as JSON")
|
|
370
529
|
.option("--wait", "Wait for completion and show result")
|
|
371
530
|
.option("--json", "Output as JSON")
|
|
372
531
|
.action(async (workflowId, options) => {
|
|
373
532
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
533
|
+
if (options.draft && options.active) {
|
|
534
|
+
error("--draft and --active are mutually exclusive");
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
374
537
|
let rootInput;
|
|
375
538
|
if (options.input) {
|
|
376
539
|
try {
|
|
@@ -386,7 +549,15 @@ Examples:
|
|
|
386
549
|
const result = await client.previewWorkflow(resolvedAppId, workflowId, {
|
|
387
550
|
rootInput,
|
|
388
551
|
configId: options.config,
|
|
552
|
+
useDraft: options.draft || false,
|
|
553
|
+
// Issue #687: --active forces the active config even when a newer
|
|
554
|
+
// draft exists (the inverse of --draft).
|
|
555
|
+
preferActive: options.active || false,
|
|
389
556
|
});
|
|
557
|
+
// Display warning from the server (e.g. "previewing draft because newer than active")
|
|
558
|
+
if (result.warning) {
|
|
559
|
+
warn(result.warning);
|
|
560
|
+
}
|
|
390
561
|
if (!options.wait) {
|
|
391
562
|
if (options.json) {
|
|
392
563
|
json(result);
|
|
@@ -394,26 +565,77 @@ Examples:
|
|
|
394
565
|
}
|
|
395
566
|
success("Preview started.");
|
|
396
567
|
keyValue("Instance ID", result.instanceId);
|
|
568
|
+
// Issue #687: name the side we ran so the user always knows which
|
|
569
|
+
// version produced the output (the "print the source" pattern).
|
|
570
|
+
const source = result.source;
|
|
571
|
+
if (source === "draft") {
|
|
572
|
+
info("Previewing draft version.");
|
|
573
|
+
}
|
|
574
|
+
else if (source === "active") {
|
|
575
|
+
// Issue #687 (review feedback): when configName is missing,
|
|
576
|
+
// fall back to a short ID prefix so users staging multiple
|
|
577
|
+
// unnamed configs can disambiguate.
|
|
578
|
+
const label = formatConfigSlotLabel("active config", result.configName, result.configId);
|
|
579
|
+
info(`Previewing ${label}.`);
|
|
580
|
+
}
|
|
581
|
+
else if (source === "config") {
|
|
582
|
+
const label = formatConfigSlotLabel("the requested config", result.configName, result.configId);
|
|
583
|
+
info(`Previewing ${label}.`);
|
|
584
|
+
}
|
|
397
585
|
info("Use 'workflows runs status' to check progress.");
|
|
398
586
|
return;
|
|
399
587
|
}
|
|
400
|
-
// Poll for completion
|
|
588
|
+
// Poll for completion. Issue #941: track whether the loop broke on a
|
|
589
|
+
// terminal state vs. ran to exhaustion, so a timed-out wait is
|
|
590
|
+
// distinguishable from a workflow that legitimately reported "running".
|
|
401
591
|
info("Waiting for completion...");
|
|
402
592
|
let status;
|
|
403
|
-
|
|
593
|
+
// Fixed ~60s cap (60 × 1s sleep + RTT). PRIMITIVE_PREVIEW_WAIT_ATTEMPTS
|
|
594
|
+
// overrides the iteration count for tests that exercise the
|
|
595
|
+
// poll-loop-exhaustion path without a full 60s wait; unset in normal use.
|
|
596
|
+
const maxAttempts = (() => {
|
|
597
|
+
const raw = process.env.PRIMITIVE_PREVIEW_WAIT_ATTEMPTS;
|
|
598
|
+
const parsed = raw ? parseInt(raw, 10) : NaN;
|
|
599
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 60;
|
|
600
|
+
})();
|
|
601
|
+
let reachedTerminal = false;
|
|
404
602
|
for (let i = 0; i < maxAttempts; i++) {
|
|
405
603
|
await new Promise((r) => setTimeout(r, 1000));
|
|
406
604
|
const statusResult = await client.getPreviewStatus(resolvedAppId, workflowId, result.instanceId);
|
|
407
605
|
status = statusResult.status;
|
|
408
|
-
|
|
606
|
+
const s = status?.status;
|
|
607
|
+
// Issue #941 (Codex P2): an aborted preview reports
|
|
608
|
+
// status:"terminated" from the admin preview-status endpoint, which
|
|
609
|
+
// is a real terminal state — the wait did NOT time out. The shared
|
|
610
|
+
// predicate includes it so the loop breaks and `waitTimedOut` stays
|
|
611
|
+
// false (vs. the prior check that omitted "terminated" and let the
|
|
612
|
+
// loop run to the cap, misreporting an abort as a timeout).
|
|
613
|
+
if (isPreviewWaitTerminalStatus(s)) {
|
|
614
|
+
reachedTerminal = true;
|
|
409
615
|
break;
|
|
410
616
|
}
|
|
411
617
|
}
|
|
618
|
+
// Issue #941: on poll-loop exhaustion with a still-non-terminal run,
|
|
619
|
+
// emit an explicit timeout signal and exit non-zero (124, the GNU
|
|
620
|
+
// `timeout` convention) instead of silently returning the bare
|
|
621
|
+
// non-terminal status as if the wait succeeded.
|
|
622
|
+
const waitTimedOut = !reachedTerminal;
|
|
412
623
|
if (options.json) {
|
|
413
|
-
|
|
624
|
+
// Additive field only — `status` is preserved so existing parsers
|
|
625
|
+
// don't break; `waitTimedOut` lets automation tell "still running,
|
|
626
|
+
// wait gave up" from "finished".
|
|
627
|
+
json({ instanceId: result.instanceId, status, waitTimedOut });
|
|
628
|
+
if (waitTimedOut) {
|
|
629
|
+
process.exit(124);
|
|
630
|
+
}
|
|
414
631
|
return;
|
|
415
632
|
}
|
|
416
|
-
|
|
633
|
+
// #2348: this render ladder used to re-derive terminality (and matched
|
|
634
|
+
// the raw "complete"/"errored" spellings the server no longer emits).
|
|
635
|
+
// It now branches on the same canonical values the poll loop stops on,
|
|
636
|
+
// via the shared sets in lib/run-status.ts.
|
|
637
|
+
const waitOutcome = previewWaitOutcome(status?.status);
|
|
638
|
+
if (waitOutcome === "completed") {
|
|
417
639
|
success("Preview completed.");
|
|
418
640
|
if (status.output) {
|
|
419
641
|
console.log("\nOutput:");
|
|
@@ -427,15 +649,45 @@ Examples:
|
|
|
427
649
|
});
|
|
428
650
|
}
|
|
429
651
|
}
|
|
430
|
-
else if (
|
|
652
|
+
else if (waitOutcome === "failed") {
|
|
431
653
|
error("Preview failed.");
|
|
432
654
|
if (status.error) {
|
|
433
655
|
console.log("\nError:");
|
|
434
656
|
console.log(JSON.stringify(status.error, null, 2));
|
|
435
657
|
}
|
|
436
658
|
}
|
|
659
|
+
else if (waitOutcome === "terminated") {
|
|
660
|
+
// Issue #941 (Codex P2): the preview was aborted (e.g. via
|
|
661
|
+
// `workflows preview ... abort`, or an external terminate). This is a
|
|
662
|
+
// real terminal state — NOT a timeout — so render it distinctly and
|
|
663
|
+
// exit 0 (the wait succeeded in observing a terminal result).
|
|
664
|
+
warn("Preview was terminated (aborted before completion).");
|
|
665
|
+
}
|
|
666
|
+
else if (waitOutcome === "skipped") {
|
|
667
|
+
// #2636 — the preview's declarative lock was held and the definition
|
|
668
|
+
// declared `onContention: "ignore"`, so it never ran. That is an
|
|
669
|
+
// outcome, not a timeout: report it and exit 0.
|
|
670
|
+
warn(`Preview did not run — lock held${status?.skipReason ? ` (${status.skipReason})` : ""}.`);
|
|
671
|
+
}
|
|
672
|
+
else if (waitOutcome === "apply") {
|
|
673
|
+
// #2348: the run finished and is parked waiting for a client to claim
|
|
674
|
+
// and apply its result. It will not advance on its own, so the wait
|
|
675
|
+
// stopping here is a success, not a timeout.
|
|
676
|
+
warn(`Preview finished and is waiting to be applied (status: ${status.status}).`);
|
|
677
|
+
if (status.output) {
|
|
678
|
+
console.log("\nOutput:");
|
|
679
|
+
console.log(JSON.stringify(status.output, null, 2));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else if (waitOutcome === "missing") {
|
|
683
|
+
// #2348: no live instance to observe — reporting a timeout here would
|
|
684
|
+
// suggest the preview is still running, which it is not.
|
|
685
|
+
error("Preview instance not found (status: missing).");
|
|
686
|
+
}
|
|
437
687
|
else {
|
|
438
|
-
|
|
688
|
+
// Timed out without ever reaching a terminal state (issue #941).
|
|
689
|
+
error(`Wait timed out after ${maxAttempts}s; preview still running. Check status with 'workflows runs status'.`);
|
|
690
|
+
process.exit(124);
|
|
439
691
|
}
|
|
440
692
|
}
|
|
441
693
|
catch (err) {
|
|
@@ -443,51 +695,401 @@ Examples:
|
|
|
443
695
|
process.exit(1);
|
|
444
696
|
}
|
|
445
697
|
});
|
|
698
|
+
// #1367 — hidden queue-latency measurement tool. Fires a batch of runs of a
|
|
699
|
+
// (trivial, no-op) workflow, polls until each has an executionStartedAt, and
|
|
700
|
+
// reports the observed queueDelayMs / createCallDurationMs distribution. This
|
|
701
|
+
// is the post-deploy verification vehicle: local miniflare does not exhibit
|
|
702
|
+
// real Cloudflare scheduling delay, so run it against an agents/alpha scratch
|
|
703
|
+
// app. Creates are paced (default 10/s) to stay clear of CF's ~100/s shared
|
|
704
|
+
// instance-creation limit; a create failure (e.g. 429) is recorded, not fatal.
|
|
705
|
+
workflows
|
|
706
|
+
.command("queue-latency-test", { hidden: true })
|
|
707
|
+
.description("(hidden) Fire N no-op runs and report the queue-delay distribution (post-deploy tool)")
|
|
708
|
+
.argument("<workflow-id>", "Workflow ID or key of a trivial no-op workflow")
|
|
709
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
710
|
+
.option("--count <n>", "Number of runs to fire", "50")
|
|
711
|
+
.option("--rate <r>", "Max creates per second (<=10 recommended)", "10")
|
|
712
|
+
.option("--input <json>", "Root input as JSON")
|
|
713
|
+
.option("--timeout <sec>", "Seconds to wait for all runs to start", "120")
|
|
714
|
+
.option("--verbose", "Print a per-run row")
|
|
715
|
+
.option("--json", "Output as JSON")
|
|
716
|
+
.action(async (workflowId, options) => {
|
|
717
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
718
|
+
const client = new ApiClient();
|
|
719
|
+
const count = Math.max(1, parseInt(options.count, 10) || 50);
|
|
720
|
+
const rate = Math.max(1, parseInt(options.rate, 10) || 10);
|
|
721
|
+
const timeoutSec = Math.max(1, parseInt(options.timeout, 10) || 120);
|
|
722
|
+
const intervalMs = Math.ceil(1000 / rate);
|
|
723
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
724
|
+
let rootInput;
|
|
725
|
+
if (options.input) {
|
|
726
|
+
try {
|
|
727
|
+
rootInput = JSON.parse(options.input);
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
error("Invalid JSON in --input");
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const fired = [];
|
|
735
|
+
if (!options.json) {
|
|
736
|
+
info(`Firing ${count} run(s) of ${workflowId} at up to ${rate}/s (interval ${intervalMs}ms)...`);
|
|
737
|
+
}
|
|
738
|
+
for (let i = 0; i < count; i++) {
|
|
739
|
+
try {
|
|
740
|
+
const res = await client.previewWorkflow(resolvedAppId, workflowId, {
|
|
741
|
+
rootInput,
|
|
742
|
+
});
|
|
743
|
+
fired.push({
|
|
744
|
+
index: i,
|
|
745
|
+
instanceId: res?.instanceId,
|
|
746
|
+
createFailed: false,
|
|
747
|
+
started: false,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
catch (err) {
|
|
751
|
+
// A create failure (e.g. HTTP 500 from CF's shared-class rate limit) is
|
|
752
|
+
// a recorded datapoint, not a reason to abort the whole batch.
|
|
753
|
+
fired.push({
|
|
754
|
+
index: i,
|
|
755
|
+
createFailed: true,
|
|
756
|
+
createError: err?.message ? String(err.message) : String(err),
|
|
757
|
+
started: false,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (i < count - 1)
|
|
761
|
+
await sleep(intervalMs);
|
|
762
|
+
}
|
|
763
|
+
// Poll the run list until every non-failed run has executionStartedAt set
|
|
764
|
+
// (queueDelayMs present) or we hit the timeout.
|
|
765
|
+
const byInstance = new Map();
|
|
766
|
+
for (const f of fired) {
|
|
767
|
+
if (f.instanceId)
|
|
768
|
+
byInstance.set(f.instanceId, f);
|
|
769
|
+
}
|
|
770
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
771
|
+
const pending = () => fired.filter((f) => !f.createFailed && !f.started).length;
|
|
772
|
+
// The admin run-list endpoint caps each page at 200, so a single large
|
|
773
|
+
// request cannot cover a batch of more than 200 runs — the earliest ones
|
|
774
|
+
// would never be matched and would be falsely reported as not started.
|
|
775
|
+
// Page through with the cursor (runs come back newest-first, so our batch
|
|
776
|
+
// is at the front) until every pending run is matched or we run out of
|
|
777
|
+
// pages. Bound the scan so we never walk the whole run history.
|
|
778
|
+
const PAGE_LIMIT = 200;
|
|
779
|
+
const scanBudget = count + PAGE_LIMIT;
|
|
780
|
+
while (pending() > 0 && Date.now() < deadline) {
|
|
781
|
+
await sleep(1000);
|
|
782
|
+
try {
|
|
783
|
+
let cursor;
|
|
784
|
+
let scanned = 0;
|
|
785
|
+
do {
|
|
786
|
+
const { items, cursor: nextCursor } = await client.listWorkflowRuns(resolvedAppId, workflowId, { limit: PAGE_LIMIT, cursor: cursor ?? undefined });
|
|
787
|
+
for (const run of items || []) {
|
|
788
|
+
const f = run?.instanceId ? byInstance.get(run.instanceId) : undefined;
|
|
789
|
+
if (f && !f.started && run.executionStartedAt) {
|
|
790
|
+
f.started = true;
|
|
791
|
+
f.queueDelayMs =
|
|
792
|
+
typeof run.queueDelayMs === "number" ? run.queueDelayMs : null;
|
|
793
|
+
f.createCallDurationMs =
|
|
794
|
+
typeof run.createCallDurationMs === "number"
|
|
795
|
+
? run.createCallDurationMs
|
|
796
|
+
: null;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
scanned += items?.length || 0;
|
|
800
|
+
cursor = nextCursor;
|
|
801
|
+
} while (pending() > 0 && cursor && scanned < scanBudget);
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
// Transient list error — keep polling until the deadline.
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const createFailures = fired.filter((f) => f.createFailed).length;
|
|
808
|
+
const startedRuns = fired.filter((f) => f.started);
|
|
809
|
+
const queueDelays = startedRuns.map((f) => f.queueDelayMs);
|
|
810
|
+
const createDurations = startedRuns.map((f) => f.createCallDurationMs);
|
|
811
|
+
const queueDist = summarizeLatencyDistribution(queueDelays);
|
|
812
|
+
const createDist = summarizeLatencyDistribution(createDurations);
|
|
813
|
+
const notStarted = fired.filter((f) => !f.createFailed && !f.started).length;
|
|
814
|
+
if (options.json) {
|
|
815
|
+
json({
|
|
816
|
+
workflowId,
|
|
817
|
+
requested: count,
|
|
818
|
+
created: count - createFailures,
|
|
819
|
+
createFailures,
|
|
820
|
+
started: startedRuns.length,
|
|
821
|
+
notStartedWithinTimeout: notStarted,
|
|
822
|
+
queueDelayMs: queueDist,
|
|
823
|
+
createCallDurationMs: createDist,
|
|
824
|
+
runs: options.verbose
|
|
825
|
+
? fired.map((f) => ({
|
|
826
|
+
index: f.index,
|
|
827
|
+
instanceId: f.instanceId ?? null,
|
|
828
|
+
createFailed: f.createFailed,
|
|
829
|
+
createError: f.createError ?? null,
|
|
830
|
+
started: f.started,
|
|
831
|
+
queueDelayMs: f.queueDelayMs ?? null,
|
|
832
|
+
createCallDurationMs: f.createCallDurationMs ?? null,
|
|
833
|
+
}))
|
|
834
|
+
: undefined,
|
|
835
|
+
});
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
divider();
|
|
839
|
+
keyValue("Requested", count);
|
|
840
|
+
keyValue("Created", count - createFailures);
|
|
841
|
+
keyValue("Create failures", createFailures);
|
|
842
|
+
keyValue("Started", startedRuns.length);
|
|
843
|
+
keyValue("Not started (timeout)", notStarted);
|
|
844
|
+
divider();
|
|
845
|
+
if (queueDist) {
|
|
846
|
+
info("Queue delay (executionStartedAt - requested):");
|
|
847
|
+
keyValue(" min", formatDuration(queueDist.min));
|
|
848
|
+
keyValue(" p50", formatDuration(queueDist.p50));
|
|
849
|
+
keyValue(" p95", formatDuration(queueDist.p95));
|
|
850
|
+
keyValue(" max", formatDuration(queueDist.max));
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
warn("No runs started — no queue-delay samples collected.");
|
|
854
|
+
}
|
|
855
|
+
if (createDist) {
|
|
856
|
+
divider();
|
|
857
|
+
info("Create-call duration:");
|
|
858
|
+
keyValue(" min", formatDuration(createDist.min));
|
|
859
|
+
keyValue(" p50", formatDuration(createDist.p50));
|
|
860
|
+
keyValue(" p95", formatDuration(createDist.p95));
|
|
861
|
+
keyValue(" max", formatDuration(createDist.max));
|
|
862
|
+
}
|
|
863
|
+
if (options.verbose) {
|
|
864
|
+
divider();
|
|
865
|
+
console.log(formatTable(fired.map((f) => ({
|
|
866
|
+
run: f.index,
|
|
867
|
+
instanceId: f.instanceId ?? "-",
|
|
868
|
+
status: f.createFailed ? "create-failed" : f.started ? "started" : "pending",
|
|
869
|
+
queueDelayMs: f.queueDelayMs,
|
|
870
|
+
createCallDurationMs: f.createCallDurationMs,
|
|
871
|
+
})), [
|
|
872
|
+
{ header: "RUN", key: "run" },
|
|
873
|
+
{ header: "INSTANCE", key: "instanceId", format: formatId },
|
|
874
|
+
{ header: "STATE", key: "status" },
|
|
875
|
+
{ header: "DELAY", key: "queueDelayMs", format: formatDuration },
|
|
876
|
+
{ header: "CREATE", key: "createCallDurationMs", format: formatDuration },
|
|
877
|
+
]));
|
|
878
|
+
}
|
|
879
|
+
});
|
|
446
880
|
// Runs subcommand
|
|
447
881
|
const runs = workflows.command("runs").description("Manage workflow runs");
|
|
448
882
|
// List runs
|
|
883
|
+
const runsTableColumns = [
|
|
884
|
+
{ header: "RUN ID", key: "runId", format: formatId },
|
|
885
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
886
|
+
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
887
|
+
// #1367 — queue delay (requested → execution start).
|
|
888
|
+
{ header: "DELAY", key: "queueDelayMs", format: formatDuration },
|
|
889
|
+
{ header: "ENDED", key: "endedAt", format: formatDate },
|
|
890
|
+
{ header: "PREVIEW", key: "isPreview", format: (v) => (v ? "yes" : "") },
|
|
891
|
+
];
|
|
449
892
|
runs
|
|
450
893
|
.command("list")
|
|
451
894
|
.description("List workflow runs")
|
|
452
|
-
|
|
895
|
+
// #1967 — the workflow id is optional when `--user-id` is given: that
|
|
896
|
+
// form lists one user's runs across every workflow.
|
|
897
|
+
.argument("[workflow-id]", "Workflow ID or key (optional with --user-id)")
|
|
453
898
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
454
|
-
|
|
899
|
+
// #2348: the canonical run-status vocabulary. The old text advertised
|
|
900
|
+
// "pending", which no server path ever writes, and omitted "terminated".
|
|
901
|
+
// An unrecognised value is now a 400 from the server, not an empty page.
|
|
902
|
+
.option("--status <status>", `Filter by status: ${RUN_STATUS_LIST}`)
|
|
903
|
+
.option("--user-id <user-id>", "List runs started by this user, across every workflow")
|
|
455
904
|
.option("--limit <n>", "Number of runs to show", "20")
|
|
905
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous page")
|
|
906
|
+
// #1969 — shared tailing flags. `--watch` re-renders the current list each
|
|
907
|
+
// interval (snapshot); `--follow` appends new/changed runs (tail). Mutually
|
|
908
|
+
// exclusive; `--interval` floor is 1s (default 2s).
|
|
909
|
+
.option("--watch", "Re-render the current run list every interval (snapshot)")
|
|
910
|
+
.option("--follow", "Append runs as they start or change (tail)")
|
|
911
|
+
.option("--interval <seconds>", "Poll interval in seconds (min 1, default 2)")
|
|
456
912
|
.option("--json", "Output as JSON")
|
|
457
913
|
.action(async (workflowId, options) => {
|
|
458
914
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
459
915
|
const client = new ApiClient();
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
916
|
+
const limit = parseInt(options.limit);
|
|
917
|
+
if (options.watch && options.follow) {
|
|
918
|
+
error("--watch and --follow are mutually exclusive; pass only one.");
|
|
919
|
+
process.exit(1);
|
|
920
|
+
}
|
|
921
|
+
// #1967 — one of the two selectors is required; `list` never enumerates
|
|
922
|
+
// every run in the app.
|
|
923
|
+
if (!workflowId && !options.userId) {
|
|
924
|
+
error("Provide a workflow ID, or --user-id to list one user's runs across every workflow.");
|
|
925
|
+
process.exit(1);
|
|
926
|
+
}
|
|
927
|
+
// The user view reads the runs-by-user index, which has no resume
|
|
928
|
+
// contract — `--follow`'s checkpoint is keyed on the by-workflow index.
|
|
929
|
+
if (options.userId && options.follow) {
|
|
930
|
+
error("--follow is not supported with --user-id (it reads a different index). Use --watch instead.");
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
// Both selectors resolve through the same page shape, so the rest of the
|
|
934
|
+
// command is identical either way.
|
|
935
|
+
const fetchPage = (params) => options.userId
|
|
936
|
+
? client.listUserWorkflowRuns(resolvedAppId, options.userId, {
|
|
937
|
+
...params,
|
|
938
|
+
// With both a workflow id and --user-id, narrow to that workflow.
|
|
939
|
+
workflowId: workflowId || undefined,
|
|
940
|
+
})
|
|
941
|
+
: client.listWorkflowRuns(resolvedAppId, workflowId, params);
|
|
942
|
+
// One-shot list (no tailing).
|
|
943
|
+
if (!options.watch && !options.follow) {
|
|
944
|
+
try {
|
|
945
|
+
const page = await fetchPage({
|
|
946
|
+
status: options.status,
|
|
947
|
+
limit,
|
|
948
|
+
cursor: options.cursor,
|
|
949
|
+
});
|
|
950
|
+
const { items } = page;
|
|
951
|
+
if (options.json) {
|
|
952
|
+
// The CLI's one list envelope with the shared inspection item
|
|
953
|
+
// shape — never a bare array, and since #3646 no `nextCursor:
|
|
954
|
+
// null` and no deprecated `cursor` alias either, because this is a
|
|
955
|
+
// `list` verb like any other (behaviors 6, 7).
|
|
956
|
+
json(buildWorkflowRunListEnvelope(items, page));
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
// The cursor hint prints whether or not this page had rows: a
|
|
960
|
+
// filtered read can come back empty with more of the index still to
|
|
961
|
+
// walk, and a bare "No runs found." would read as "this user has no
|
|
962
|
+
// matching runs" (#1967 review).
|
|
963
|
+
const printCursorHint = () => {
|
|
964
|
+
if (!page.nextCursor)
|
|
965
|
+
return;
|
|
966
|
+
console.log();
|
|
967
|
+
dim(`More runs. Use --cursor ${page.nextCursor} for the next page.`);
|
|
968
|
+
};
|
|
969
|
+
if (!items || items.length === 0) {
|
|
970
|
+
// #2237 — the honest empty state: a bounded filtered read can stop
|
|
971
|
+
// before it reaches a match, so the message says how far it got and
|
|
972
|
+
// how to continue instead of asserting there are none. `runs list`
|
|
973
|
+
// deliberately does NOT auto-resume like `runs failures` — it is a
|
|
974
|
+
// pager, and its `--limit` is a page size.
|
|
975
|
+
info(describeEmptyRunPage(page, !!options.cursor));
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
console.log(formatTable(items, runsTableColumns));
|
|
979
|
+
printCursorHint();
|
|
468
980
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
981
|
+
catch (err) {
|
|
982
|
+
error(err.message);
|
|
983
|
+
process.exit(1);
|
|
472
984
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
// Tailing (`--watch` / `--follow`). SIGINT → AbortController installed
|
|
988
|
+
// here at the command level; the handler is removed in `finally` so it
|
|
989
|
+
// never leaks past this command (unlike the persistent process.on).
|
|
990
|
+
let intervalSeconds;
|
|
991
|
+
try {
|
|
992
|
+
intervalSeconds = parseIntervalSeconds(options.interval);
|
|
480
993
|
}
|
|
481
994
|
catch (err) {
|
|
482
995
|
error(err.message);
|
|
483
996
|
process.exit(1);
|
|
484
997
|
}
|
|
998
|
+
const intervalMs = intervalSeconds * 1000;
|
|
999
|
+
const controller = new AbortController();
|
|
1000
|
+
const onSigint = () => controller.abort();
|
|
1001
|
+
process.on("SIGINT", onSigint);
|
|
1002
|
+
try {
|
|
1003
|
+
if (options.follow) {
|
|
1004
|
+
const source = {
|
|
1005
|
+
fetchAfter: async (resumeAfter) => {
|
|
1006
|
+
if (resumeAfter === undefined) {
|
|
1007
|
+
// Bootstrap: newest position, descending, one row. followLoop
|
|
1008
|
+
// discards the items and keeps only the baseline checkpoint.
|
|
1009
|
+
const page = await client.listWorkflowRuns(resolvedAppId, workflowId, {
|
|
1010
|
+
follow: true,
|
|
1011
|
+
forward: false,
|
|
1012
|
+
limit: 1,
|
|
1013
|
+
status: options.status,
|
|
1014
|
+
});
|
|
1015
|
+
return { items: page.items, hasMore: false, resumeAfter: page.resumeAfter };
|
|
1016
|
+
}
|
|
1017
|
+
const page = await client.listWorkflowRuns(resolvedAppId, workflowId, {
|
|
1018
|
+
follow: true,
|
|
1019
|
+
forward: true,
|
|
1020
|
+
after: resumeAfter,
|
|
1021
|
+
limit,
|
|
1022
|
+
status: options.status,
|
|
1023
|
+
});
|
|
1024
|
+
return {
|
|
1025
|
+
items: page.items,
|
|
1026
|
+
hasMore: !!page.hasMore,
|
|
1027
|
+
resumeAfter: page.resumeAfter,
|
|
1028
|
+
};
|
|
1029
|
+
},
|
|
1030
|
+
};
|
|
1031
|
+
if (!options.json) {
|
|
1032
|
+
info("Following runs (Ctrl-C to stop)…");
|
|
1033
|
+
}
|
|
1034
|
+
await followLoop(source, {
|
|
1035
|
+
intervalMs,
|
|
1036
|
+
signal: controller.signal,
|
|
1037
|
+
render: (items) => {
|
|
1038
|
+
if (options.json) {
|
|
1039
|
+
// One JSON object per line (a tail has no closing bracket to
|
|
1040
|
+
// wait for), each in the shared inspection item shape — the
|
|
1041
|
+
// same items the one-shot envelope carries (behavior 7).
|
|
1042
|
+
for (const run of items)
|
|
1043
|
+
jsonLine(toWorkflowRunInspectionRow(run));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
for (const run of items) {
|
|
1047
|
+
console.log(`${formatDate(run.startedAt)} ${formatStatus(run.status)} ${formatId(run.runId)}`);
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
// --watch: periodic snapshot redraw. The most recent page's
|
|
1054
|
+
// pagination fields are kept so the `--json` redraw emits the same
|
|
1055
|
+
// envelope the one-shot path does, not a bare array (behavior 7).
|
|
1056
|
+
let lastPage = {};
|
|
1057
|
+
await watchLoop(async () => {
|
|
1058
|
+
const page = await fetchPage({
|
|
1059
|
+
status: options.status,
|
|
1060
|
+
limit,
|
|
1061
|
+
cursor: options.cursor,
|
|
1062
|
+
});
|
|
1063
|
+
lastPage = page;
|
|
1064
|
+
return page.items;
|
|
1065
|
+
}, {
|
|
1066
|
+
intervalMs,
|
|
1067
|
+
signal: controller.signal,
|
|
1068
|
+
render: (items) => {
|
|
1069
|
+
if (options.json) {
|
|
1070
|
+
json(buildWorkflowRunListEnvelope(items, lastPage));
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
// Renderer owns the redraw: clear the screen, then print.
|
|
1074
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
1075
|
+
if (!items || items.length === 0) {
|
|
1076
|
+
console.log("No runs found.");
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
console.log(formatTable(items, runsTableColumns));
|
|
1080
|
+
},
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
finally {
|
|
1085
|
+
process.removeListener("SIGINT", onSigint);
|
|
1086
|
+
}
|
|
485
1087
|
});
|
|
486
1088
|
// Run status
|
|
487
1089
|
runs
|
|
488
1090
|
.command("status")
|
|
489
1091
|
.description("Get status of a workflow run")
|
|
490
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1092
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
491
1093
|
.argument("<run-id>", "Run ID")
|
|
492
1094
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
493
1095
|
.option("--json", "Output as JSON")
|
|
@@ -496,310 +1098,620 @@ Examples:
|
|
|
496
1098
|
const client = new ApiClient();
|
|
497
1099
|
try {
|
|
498
1100
|
const result = await client.getWorkflowRunStatus(resolvedAppId, workflowId, runId);
|
|
1101
|
+
// #2771 — a Cloudflare instance reports `stepResults` only once the run
|
|
1102
|
+
// is terminal, so a live run used to answer with `status: running` and
|
|
1103
|
+
// nothing else for its whole duration. Step rows are now written as the
|
|
1104
|
+
// run goes, so fall back to them: the steps finished so far plus the one
|
|
1105
|
+
// currently executing. Resolved BEFORE the output branches — a script
|
|
1106
|
+
// polling `--json` needs the in-flight trace as much as a human does.
|
|
1107
|
+
// Best-effort: a lookup failure still leaves the run details intact.
|
|
1108
|
+
const instanceStepResults = result.instanceStatus?.stepResults;
|
|
1109
|
+
let persistedStepRuns = [];
|
|
1110
|
+
if (!instanceStepResults || instanceStepResults.length === 0) {
|
|
1111
|
+
try {
|
|
1112
|
+
const stepRuns = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
|
|
1113
|
+
persistedStepRuns = stepRuns?.items || [];
|
|
1114
|
+
}
|
|
1115
|
+
catch {
|
|
1116
|
+
persistedStepRuns = [];
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
499
1119
|
if (options.json) {
|
|
500
|
-
|
|
1120
|
+
// Purely additive: `stepRuns` appears only when the persisted rows are
|
|
1121
|
+
// what carries the trace, so a terminal run's JSON is unchanged.
|
|
1122
|
+
json(persistedStepRuns.length > 0 ? { ...result, stepRuns: persistedStepRuns } : result);
|
|
501
1123
|
return;
|
|
502
1124
|
}
|
|
503
1125
|
const run = result.run;
|
|
504
1126
|
keyValue("Run ID", run.runId);
|
|
505
1127
|
keyValue("Status", formatStatus(run.status));
|
|
506
|
-
|
|
1128
|
+
// #2636 — the machine-readable outcome next to the status: why a
|
|
1129
|
+
// failed run failed, or why an elided run never ran.
|
|
1130
|
+
if (run.errorCode)
|
|
1131
|
+
keyValue("Error code", run.errorCode);
|
|
1132
|
+
if (run.skipReason)
|
|
1133
|
+
keyValue("Skip reason", run.skipReason);
|
|
1134
|
+
// #1367 — `startedAt` is the request time; `executionStartedAt` is when
|
|
1135
|
+
// Cloudflare actually started running the instance. Queue delay is the
|
|
1136
|
+
// gap. A still-queued run shows "-" for execution start / queue delay.
|
|
1137
|
+
keyValue("Requested", formatDate(run.startedAt));
|
|
1138
|
+
keyValue("Execution started", formatDate(run.executionStartedAt));
|
|
1139
|
+
keyValue("Queue delay", formatDuration(run.queueDelayMs));
|
|
1140
|
+
keyValue("Create call", formatDuration(run.createCallDurationMs));
|
|
507
1141
|
keyValue("Ended", formatDate(run.endedAt));
|
|
508
1142
|
keyValue("Preview", run.isPreview ? "yes" : "no");
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
console.log(` ${step.id}: ${formatDuration(step.durationMs)} ${step.skipped ? "(skipped)" : ""}`);
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
if (status.error) {
|
|
524
|
-
divider();
|
|
525
|
-
error("Error:");
|
|
526
|
-
console.log(JSON.stringify(status.error, null, 2));
|
|
1143
|
+
const stepResultLines = renderRunStatusStepResults(stepResultsForRunStatus(instanceStepResults, persistedStepRuns));
|
|
1144
|
+
if (result.instanceStatus?.output) {
|
|
1145
|
+
divider();
|
|
1146
|
+
info("Output:");
|
|
1147
|
+
console.log(JSON.stringify(result.instanceStatus.output, null, 2));
|
|
1148
|
+
}
|
|
1149
|
+
if (stepResultLines.length > 0) {
|
|
1150
|
+
divider();
|
|
1151
|
+
info("Step Results:");
|
|
1152
|
+
for (const line of stepResultLines) {
|
|
1153
|
+
console.log(line);
|
|
527
1154
|
}
|
|
528
1155
|
}
|
|
1156
|
+
if (result.instanceStatus?.error) {
|
|
1157
|
+
divider();
|
|
1158
|
+
error("Error:");
|
|
1159
|
+
console.log(JSON.stringify(result.instanceStatus.error, null, 2));
|
|
1160
|
+
}
|
|
529
1161
|
}
|
|
530
1162
|
catch (err) {
|
|
531
1163
|
error(err.message);
|
|
532
1164
|
process.exit(1);
|
|
533
1165
|
}
|
|
534
1166
|
});
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
.
|
|
540
|
-
.
|
|
541
|
-
.
|
|
542
|
-
Examples:
|
|
543
|
-
$ primitive workflows configs list <workflow-id>
|
|
544
|
-
$ primitive workflows configs get <workflow-id> <config-id>
|
|
545
|
-
$ primitive workflows configs create <workflow-id> --name "v2" --from-file workflow.toml
|
|
546
|
-
$ primitive workflows configs activate <workflow-id> <config-id>
|
|
547
|
-
$ primitive workflows configs duplicate <workflow-id> <config-id> --name "v2-copy"
|
|
548
|
-
`);
|
|
549
|
-
// List configs
|
|
550
|
-
configs
|
|
551
|
-
.command("list")
|
|
552
|
-
.description("List configurations for a workflow")
|
|
553
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
554
|
-
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1167
|
+
// Steps for a run
|
|
1168
|
+
runs
|
|
1169
|
+
.command("steps")
|
|
1170
|
+
.description("Show step-level details for a workflow run")
|
|
1171
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1172
|
+
.argument("<run-id>", "Run ID")
|
|
1173
|
+
.option("--app <app-id>", "App ID")
|
|
555
1174
|
.option("--json", "Output as JSON")
|
|
556
|
-
.action(async (workflowId, options) => {
|
|
1175
|
+
.action(async (workflowId, runId, options) => {
|
|
557
1176
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
558
1177
|
const client = new ApiClient();
|
|
559
1178
|
try {
|
|
560
|
-
const { items } = await client.
|
|
1179
|
+
const { items } = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
|
|
561
1180
|
if (options.json) {
|
|
562
|
-
|
|
1181
|
+
// Steps are unpaginated (one run's full trace) — the shared
|
|
1182
|
+
// inspection item shape wrapped in `{items}`, no cursor (behavior 6).
|
|
1183
|
+
json(buildWorkflowStepEnvelope(items));
|
|
563
1184
|
return;
|
|
564
1185
|
}
|
|
565
1186
|
if (!items || items.length === 0) {
|
|
566
|
-
info("No
|
|
1187
|
+
info("No step runs found.");
|
|
567
1188
|
return;
|
|
568
1189
|
}
|
|
569
|
-
|
|
570
|
-
const { workflow } = await client.getWorkflow(resolvedAppId, workflowId);
|
|
571
|
-
const activeConfigId = workflow?.activeConfigId;
|
|
572
|
-
console.log(formatTable(items, [
|
|
573
|
-
{ header: "ID", key: "configId", format: formatId },
|
|
574
|
-
{ header: "NAME", key: "configName" },
|
|
575
|
-
{ header: "STATUS", key: "status", format: formatStatus },
|
|
576
|
-
{
|
|
577
|
-
header: "ACTIVE",
|
|
578
|
-
key: "configId",
|
|
579
|
-
format: (v) => v === activeConfigId ? "★" : "",
|
|
580
|
-
},
|
|
581
|
-
{ header: "MODIFIED", key: "modifiedAt", format: formatDate },
|
|
582
|
-
]));
|
|
1190
|
+
console.log(renderStepRunTable(items));
|
|
583
1191
|
}
|
|
584
1192
|
catch (err) {
|
|
585
1193
|
error(err.message);
|
|
586
1194
|
process.exit(1);
|
|
587
1195
|
}
|
|
588
1196
|
});
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
.command("
|
|
592
|
-
.description("
|
|
593
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
594
|
-
.argument("<
|
|
595
|
-
.
|
|
1197
|
+
// Step detail
|
|
1198
|
+
runs
|
|
1199
|
+
.command("step-detail")
|
|
1200
|
+
.description("Show full details for a single step")
|
|
1201
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1202
|
+
.argument("<run-id>", "Run ID")
|
|
1203
|
+
.argument("<step-id>", "Step ID")
|
|
1204
|
+
.option("--app <app-id>", "App ID")
|
|
596
1205
|
.option("--json", "Output as JSON")
|
|
597
|
-
.action(async (workflowId,
|
|
1206
|
+
.action(async (workflowId, runId, stepId, options) => {
|
|
598
1207
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
599
1208
|
const client = new ApiClient();
|
|
600
1209
|
try {
|
|
601
|
-
const
|
|
1210
|
+
const { items } = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
|
|
1211
|
+
const step = items.find((s) => s.stepId === stepId);
|
|
1212
|
+
if (!step) {
|
|
1213
|
+
error(`Step "${stepId}" not found in run ${runId}`);
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
}
|
|
602
1216
|
if (options.json) {
|
|
603
|
-
json(
|
|
1217
|
+
json(step);
|
|
604
1218
|
return;
|
|
605
1219
|
}
|
|
606
|
-
keyValue("
|
|
607
|
-
keyValue("
|
|
608
|
-
keyValue("
|
|
609
|
-
keyValue("
|
|
610
|
-
keyValue("
|
|
611
|
-
keyValue("
|
|
612
|
-
if (
|
|
1220
|
+
keyValue("Step", step.stepId);
|
|
1221
|
+
keyValue("Kind", step.stepKind);
|
|
1222
|
+
keyValue("Status", formatStatus(step.status));
|
|
1223
|
+
keyValue("Duration", formatDuration(step.durationMs));
|
|
1224
|
+
keyValue("Started", formatDate(step.startedAt));
|
|
1225
|
+
keyValue("Ended", formatDate(step.endedAt));
|
|
1226
|
+
if (step.totalTokens) {
|
|
1227
|
+
keyValue("Tokens", `${step.inputTokens || 0} in / ${step.outputTokens || 0} out / ${step.totalTokens} total`);
|
|
1228
|
+
}
|
|
1229
|
+
if (step.retryCount) {
|
|
1230
|
+
keyValue("Retries", String(step.retryCount));
|
|
1231
|
+
}
|
|
1232
|
+
if (step.config) {
|
|
613
1233
|
divider();
|
|
614
|
-
info(
|
|
615
|
-
|
|
616
|
-
|
|
1234
|
+
info("Config (rendered):");
|
|
1235
|
+
console.log(JSON.stringify(step.config, null, 2));
|
|
1236
|
+
}
|
|
1237
|
+
if (step.rawConfig) {
|
|
1238
|
+
divider();
|
|
1239
|
+
info("Config (raw):");
|
|
1240
|
+
console.log(JSON.stringify(step.rawConfig, null, 2));
|
|
1241
|
+
}
|
|
1242
|
+
if (step.input) {
|
|
1243
|
+
divider();
|
|
1244
|
+
info("Input:");
|
|
1245
|
+
console.log(JSON.stringify(step.input, null, 2));
|
|
1246
|
+
}
|
|
1247
|
+
if (step.output) {
|
|
1248
|
+
divider();
|
|
1249
|
+
info("Output:");
|
|
1250
|
+
console.log(JSON.stringify(step.output, null, 2));
|
|
1251
|
+
}
|
|
1252
|
+
if (step.error) {
|
|
1253
|
+
divider();
|
|
1254
|
+
error("Error: " + step.error);
|
|
1255
|
+
if (step.errorDetails) {
|
|
1256
|
+
console.log(step.errorDetails);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
if (step.templateWarnings && step.templateWarnings.length > 0) {
|
|
1260
|
+
divider();
|
|
1261
|
+
warn("Template Warnings:");
|
|
1262
|
+
step.templateWarnings.forEach((w) => {
|
|
1263
|
+
console.log(` ${w.expression}: ${w.message}`);
|
|
617
1264
|
});
|
|
618
1265
|
}
|
|
1266
|
+
if (step.context) {
|
|
1267
|
+
divider();
|
|
1268
|
+
info("Context Snapshot:");
|
|
1269
|
+
console.log(JSON.stringify(step.context, null, 2));
|
|
1270
|
+
}
|
|
619
1271
|
}
|
|
620
1272
|
catch (err) {
|
|
621
1273
|
error(err.message);
|
|
622
1274
|
process.exit(1);
|
|
623
1275
|
}
|
|
624
1276
|
});
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
.
|
|
629
|
-
.
|
|
1277
|
+
// Integration call logs for a run (cross-pivot from workflow-run to
|
|
1278
|
+
// integrations.logs — see issue #699).
|
|
1279
|
+
runs
|
|
1280
|
+
.command("logs")
|
|
1281
|
+
.description("List integration calls made by a specific workflow run (cross-pivot of `integrations logs`)")
|
|
1282
|
+
.argument("<run-id>", "Workflow run ID")
|
|
630
1283
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
631
|
-
.option("--
|
|
632
|
-
.option("--description <desc>", "Description")
|
|
633
|
-
.option("--from-file <path>", "Load steps from TOML file")
|
|
1284
|
+
.option("--limit <n>", "Number of logs to show", "100")
|
|
634
1285
|
.option("--json", "Output as JSON")
|
|
635
|
-
.action(async (
|
|
1286
|
+
.action(async (runId, options) => {
|
|
636
1287
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
637
|
-
|
|
638
|
-
|
|
1288
|
+
const client = new ApiClient();
|
|
1289
|
+
try {
|
|
1290
|
+
const logs = await client.listWorkflowRunIntegrationLogs(resolvedAppId, runId, { limit: parseInt(options.limit) });
|
|
1291
|
+
if (options.json) {
|
|
1292
|
+
json(logs);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (!logs || logs.length === 0) {
|
|
1296
|
+
info("No integration calls found for this run.");
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
console.log(formatTable(logs, [
|
|
1300
|
+
{ header: "TIME", key: "timestamp", format: formatDate },
|
|
1301
|
+
{ header: "STEP", key: "stepId", format: (v) => v || "" },
|
|
1302
|
+
{
|
|
1303
|
+
header: "INTEGRATION",
|
|
1304
|
+
key: "integrationKey",
|
|
1305
|
+
format: (v) => v || "",
|
|
1306
|
+
},
|
|
1307
|
+
{ header: "METHOD", key: "method" },
|
|
1308
|
+
{ header: "PATH", key: "path" },
|
|
1309
|
+
{ header: "STATUS", key: "status" },
|
|
1310
|
+
{ header: "DURATION", key: "durationMs", format: formatDuration },
|
|
1311
|
+
{ header: "TRACE", key: "traceId", format: formatId },
|
|
1312
|
+
]));
|
|
1313
|
+
}
|
|
1314
|
+
catch (err) {
|
|
1315
|
+
error(err.message);
|
|
639
1316
|
process.exit(1);
|
|
640
1317
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1318
|
+
});
|
|
1319
|
+
// Error summary
|
|
1320
|
+
runs
|
|
1321
|
+
.command("error")
|
|
1322
|
+
.description("Show error details for a failed workflow run")
|
|
1323
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1324
|
+
.argument("<run-id>", "Run ID")
|
|
1325
|
+
.option("--app <app-id>", "App ID")
|
|
1326
|
+
.option("--json", "Output as JSON")
|
|
1327
|
+
.action(async (workflowId, runId, options) => {
|
|
1328
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1329
|
+
const client = new ApiClient();
|
|
1330
|
+
try {
|
|
1331
|
+
const [runResult, stepsResult] = await Promise.all([
|
|
1332
|
+
client.getWorkflowRunStatus(resolvedAppId, workflowId, runId),
|
|
1333
|
+
client.getWorkflowStepRuns(resolvedAppId, workflowId, runId),
|
|
1334
|
+
]);
|
|
1335
|
+
// #2074 — one selection rule shared with the STEP column of
|
|
1336
|
+
// `runs failures`: prefer the run's own denormalized attribution, and
|
|
1337
|
+
// fall back to the lowest-`stepIndex` failed row (never "whatever the
|
|
1338
|
+
// query returned first") so the two views can't disagree.
|
|
1339
|
+
const failedStep = selectFailedStepRow(stepsResult.items, runResult.run?.failedStepId);
|
|
1340
|
+
const errorInfo = {
|
|
1341
|
+
runId,
|
|
1342
|
+
status: runResult.run?.status,
|
|
1343
|
+
// #2636 — the platform's classification of the failure, when it owns
|
|
1344
|
+
// one (a lost declarative-lock race, an exhausted lock wait).
|
|
1345
|
+
errorCode: runResult.run?.errorCode ?? null,
|
|
1346
|
+
errorMessage: runResult.run?.errorMessage || runResult.instanceStatus?.error?.message,
|
|
1347
|
+
failedStep: failedStep ? {
|
|
1348
|
+
stepId: failedStep.stepId,
|
|
1349
|
+
stepKind: failedStep.stepKind,
|
|
1350
|
+
error: failedStep.error,
|
|
1351
|
+
errorDetails: failedStep.errorDetails,
|
|
1352
|
+
input: failedStep.input,
|
|
1353
|
+
config: failedStep.config,
|
|
1354
|
+
} : null,
|
|
1355
|
+
};
|
|
1356
|
+
if (options.json) {
|
|
1357
|
+
json(errorInfo);
|
|
1358
|
+
return;
|
|
647
1359
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1360
|
+
keyValue("Run ID", runId);
|
|
1361
|
+
keyValue("Status", formatStatus(errorInfo.status));
|
|
1362
|
+
if (errorInfo.errorCode) {
|
|
1363
|
+
keyValue("Code", errorInfo.errorCode);
|
|
1364
|
+
}
|
|
1365
|
+
if (errorInfo.errorMessage) {
|
|
1366
|
+
divider();
|
|
1367
|
+
error("Error: " + errorInfo.errorMessage);
|
|
1368
|
+
}
|
|
1369
|
+
if (failedStep) {
|
|
1370
|
+
divider();
|
|
1371
|
+
info(`Failed Step: ${failedStep.stepId} (${failedStep.stepKind})`);
|
|
1372
|
+
if (failedStep.error) {
|
|
1373
|
+
error(" " + failedStep.error);
|
|
1374
|
+
}
|
|
1375
|
+
if (failedStep.errorDetails) {
|
|
1376
|
+
console.log(failedStep.errorDetails);
|
|
1377
|
+
}
|
|
1378
|
+
if (failedStep.input) {
|
|
1379
|
+
divider();
|
|
1380
|
+
info("Step Input:");
|
|
1381
|
+
console.log(JSON.stringify(failedStep.input, null, 2));
|
|
1382
|
+
}
|
|
1383
|
+
if (failedStep.config) {
|
|
1384
|
+
divider();
|
|
1385
|
+
info("Step Config:");
|
|
1386
|
+
console.log(JSON.stringify(failedStep.config, null, 2));
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
info("No failed step found in step runs.");
|
|
651
1391
|
}
|
|
652
1392
|
}
|
|
1393
|
+
catch (err) {
|
|
1394
|
+
error(err.message);
|
|
1395
|
+
process.exit(1);
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
// Failures list
|
|
1399
|
+
runs
|
|
1400
|
+
.command("failures")
|
|
1401
|
+
.description("List recent workflow run failures")
|
|
1402
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1403
|
+
.option("--app <app-id>", "App ID")
|
|
1404
|
+
// #2237 — `--limit` is the number of FAILURES to collect, as the help text
|
|
1405
|
+
// always claimed. It used to be passed straight through as the server's
|
|
1406
|
+
// bounded page limit, so a workflow with a long success streak answered
|
|
1407
|
+
// "No failures found." while failures sat just past the scan budget.
|
|
1408
|
+
.option("--limit <n>", "Number of failures to collect", "10")
|
|
1409
|
+
.option("--max-scan <n>", `Stop after examining this many runs, to the nearest request (each examines up to ${SERVER_SCAN_ROWS_PER_REQUEST}; 0 = no cap; a sweep issues at most ${FAILURE_SWEEP_MAX_REQUESTS} requests either way)`, String(DEFAULT_FAILURE_SWEEP_MAX_SCAN))
|
|
1410
|
+
// `--cursor` seeds the sweep's starting position; the sweep then resumes
|
|
1411
|
+
// itself, so it is only needed to continue an earlier capped search.
|
|
1412
|
+
.option("--cursor <cursor>", "Resume the search from a previous cursor")
|
|
1413
|
+
.option("--json", "Output as JSON")
|
|
1414
|
+
.action(async (workflowId, options) => {
|
|
1415
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
653
1416
|
const client = new ApiClient();
|
|
1417
|
+
const limit = parseInt(options.limit, 10);
|
|
1418
|
+
const maxScan = parseInt(options.maxScan, 10);
|
|
1419
|
+
if (!Number.isFinite(limit) || limit < 1) {
|
|
1420
|
+
error("--limit must be a positive integer.");
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
if (!Number.isFinite(maxScan) || maxScan < 0) {
|
|
1424
|
+
error("--max-scan must be a non-negative integer (0 = no cap).");
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
// Ctrl-C during a multi-second sweep prints what was collected plus the
|
|
1428
|
+
// resume cursor rather than dropping the work on the floor. The handler
|
|
1429
|
+
// is removed in `finally` so it never leaks past this command.
|
|
1430
|
+
//
|
|
1431
|
+
// Registering a listener replaces Node's terminate-on-SIGINT default, and
|
|
1432
|
+
// `shouldStop` is only read between requests — so a first Ctrl-C during a
|
|
1433
|
+
// slow in-flight request would otherwise appear to do nothing and leave
|
|
1434
|
+
// the command unkillable. A second one exits immediately (#2237 review).
|
|
1435
|
+
let interrupted = false;
|
|
1436
|
+
let sigints = 0;
|
|
1437
|
+
const onSigint = () => {
|
|
1438
|
+
interrupted = true;
|
|
1439
|
+
if (++sigints > 1)
|
|
1440
|
+
process.exit(130);
|
|
1441
|
+
};
|
|
1442
|
+
process.on("SIGINT", onSigint);
|
|
1443
|
+
// Progress on stderr, TTY only, never in `--json` mode: a silent
|
|
1444
|
+
// multi-second command invites the Ctrl-C it is trying to survive.
|
|
1445
|
+
const showProgress = !options.json && !!process.stderr.isTTY;
|
|
654
1446
|
try {
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1447
|
+
const sweep = await collectWorkflowFailures(({ cursor, limit: pageLimit }) => client.listWorkflowRuns(resolvedAppId, workflowId, {
|
|
1448
|
+
status: "failed",
|
|
1449
|
+
limit: pageLimit,
|
|
1450
|
+
cursor,
|
|
1451
|
+
}), {
|
|
1452
|
+
limit,
|
|
1453
|
+
maxScan,
|
|
1454
|
+
startCursor: options.cursor,
|
|
1455
|
+
shouldStop: () => interrupted,
|
|
1456
|
+
onProgress: showProgress
|
|
1457
|
+
? (scanned, collected) => progress(`Searching… ${scanned} runs examined, ${collected} failure${collected === 1 ? "" : "s"} found`)
|
|
1458
|
+
: undefined,
|
|
659
1459
|
});
|
|
1460
|
+
if (showProgress && sweep.requests >= 1) {
|
|
1461
|
+
// Clear the progress line so it does not sit above the table. One
|
|
1462
|
+
// request is the common case — `onProgress` fires after EVERY request
|
|
1463
|
+
// including the first, so guarding on `> 1` left the line on screen
|
|
1464
|
+
// with the cursor parked at its end (#2237 review).
|
|
1465
|
+
process.stderr.write("\r\x1b[K");
|
|
1466
|
+
}
|
|
1467
|
+
// A SIGINT that arrived after the sweep returned (while the table is
|
|
1468
|
+
// rendering) did not truncate anything, so only a sweep that actually
|
|
1469
|
+
// stopped on the interrupt reports 130. `exitCode` rather than
|
|
1470
|
+
// `process.exit()` so a piped stdout is not truncated mid-write.
|
|
1471
|
+
const exitInterrupted = sweep.stoppedBecause === "interrupted";
|
|
1472
|
+
const items = sweep.items;
|
|
660
1473
|
if (options.json) {
|
|
661
|
-
|
|
1474
|
+
// #2074 — the shared inspection item shape inside the endpoint's own
|
|
1475
|
+
// #1316 envelope, never a bare array. #2237 — `items` is the
|
|
1476
|
+
// concatenation across every resumed page, `scanned` their sum, and
|
|
1477
|
+
// `nextCursor` the last one observed.
|
|
1478
|
+
json(buildWorkflowRunEnvelope(items, {
|
|
1479
|
+
nextCursor: sweep.nextCursor,
|
|
1480
|
+
hasMore: !!sweep.nextCursor,
|
|
1481
|
+
...(sweep.scannedKnown ? { scanned: sweep.scanned } : {}),
|
|
1482
|
+
}));
|
|
1483
|
+
if (exitInterrupted)
|
|
1484
|
+
process.exitCode = 130;
|
|
662
1485
|
return;
|
|
663
1486
|
}
|
|
664
|
-
|
|
665
|
-
|
|
1487
|
+
if (items.length > 0) {
|
|
1488
|
+
console.log(formatTable(items, [
|
|
1489
|
+
{ header: "RUN ID", key: "runId", format: formatId },
|
|
1490
|
+
// STEP sits before ERROR so the truncated message stays the last
|
|
1491
|
+
// wide column and the step id is never the thing that gets cut.
|
|
1492
|
+
// `-` is a routine outcome, not a bug: launch-time failures
|
|
1493
|
+
// (create/schema/R2 aborts, reclaimed runs) legitimately have no
|
|
1494
|
+
// failed step, and runs that predate #2074 carry no attribution.
|
|
1495
|
+
{ header: "STEP", key: "failedStepId", format: formatFailureStepCell },
|
|
1496
|
+
// #2636 — the platform's classification, next to the message an
|
|
1497
|
+
// operator would otherwise have had to pattern-match.
|
|
1498
|
+
{ header: "CODE", key: "errorCode", format: formatFailureCodeCell },
|
|
1499
|
+
{ header: "ERROR", key: "errorMessage", format: formatFailureErrorCell },
|
|
1500
|
+
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
1501
|
+
{ header: "ENDED", key: "endedAt", format: formatDate },
|
|
1502
|
+
]));
|
|
1503
|
+
}
|
|
1504
|
+
// #2237 — replaces the unconditional "No failures found.": every form
|
|
1505
|
+
// either states the search was complete or says how far it got and how
|
|
1506
|
+
// to continue. A triage command must never assert an absence it did
|
|
1507
|
+
// not establish.
|
|
1508
|
+
const summary = describeFailureSweep(sweep, limit);
|
|
1509
|
+
if (summary) {
|
|
1510
|
+
if (items.length > 0)
|
|
1511
|
+
console.log();
|
|
1512
|
+
info(summary);
|
|
1513
|
+
}
|
|
1514
|
+
if (exitInterrupted)
|
|
1515
|
+
process.exitCode = 130;
|
|
666
1516
|
}
|
|
667
1517
|
catch (err) {
|
|
668
1518
|
error(err.message);
|
|
669
1519
|
process.exit(1);
|
|
670
1520
|
}
|
|
1521
|
+
finally {
|
|
1522
|
+
process.removeListener("SIGINT", onSigint);
|
|
1523
|
+
}
|
|
671
1524
|
});
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
.command("
|
|
675
|
-
.description("
|
|
676
|
-
|
|
677
|
-
.
|
|
1525
|
+
// ── Iterations: inspect / reset iterate-users singleton iterations (#1209) ──
|
|
1526
|
+
const iterations = workflows
|
|
1527
|
+
.command("iterations")
|
|
1528
|
+
.description("Inspect and reset iterate-users iterations");
|
|
1529
|
+
iterations
|
|
1530
|
+
.command("list")
|
|
1531
|
+
.description("List iterate-users iterations for the app")
|
|
678
1532
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
679
|
-
.option("--name <name>", "Configuration name")
|
|
680
|
-
.option("--description <desc>", "Description")
|
|
681
|
-
.option("--from-file <path>", "Update steps from TOML file")
|
|
682
1533
|
.option("--json", "Output as JSON")
|
|
683
|
-
.action(async (
|
|
1534
|
+
.action(async (options) => {
|
|
684
1535
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
try {
|
|
692
|
-
const content = readFileSync(options.fromFile, "utf-8");
|
|
693
|
-
const tomlData = TOML.parse(content);
|
|
694
|
-
payload.steps = tomlData.steps || [];
|
|
1536
|
+
const client = new ApiClient();
|
|
1537
|
+
try {
|
|
1538
|
+
const { items } = await client.listIterations(resolvedAppId);
|
|
1539
|
+
if (options.json) {
|
|
1540
|
+
json(wholeListEnvelope(items));
|
|
1541
|
+
return;
|
|
695
1542
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1543
|
+
if (!items || items.length === 0) {
|
|
1544
|
+
info("No iterations found.");
|
|
1545
|
+
return;
|
|
699
1546
|
}
|
|
1547
|
+
console.log(formatTable(items, [
|
|
1548
|
+
{ header: "ITERATION", key: "iterationName" },
|
|
1549
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
1550
|
+
{ header: "ACQUIRED", key: "lastAcquireMode", format: (v) => v || "" },
|
|
1551
|
+
{ header: "PROCESSED", key: "totalProcessed" },
|
|
1552
|
+
{ header: "FAILED", key: "failedCount" },
|
|
1553
|
+
{ header: "LAST RUN", key: "lastRunAt", format: formatDate },
|
|
1554
|
+
]));
|
|
700
1555
|
}
|
|
701
|
-
|
|
702
|
-
error(
|
|
1556
|
+
catch (err) {
|
|
1557
|
+
error(err.message);
|
|
703
1558
|
process.exit(1);
|
|
704
1559
|
}
|
|
1560
|
+
});
|
|
1561
|
+
iterations
|
|
1562
|
+
.command("get")
|
|
1563
|
+
.description("Show details for a single iterate-users iteration")
|
|
1564
|
+
.argument("<name>", "Iteration name")
|
|
1565
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1566
|
+
.option("--json", "Output as JSON")
|
|
1567
|
+
.action(async (name, options) => {
|
|
1568
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
705
1569
|
const client = new ApiClient();
|
|
706
1570
|
try {
|
|
707
|
-
const
|
|
1571
|
+
const it = await client.getIteration(resolvedAppId, name);
|
|
708
1572
|
if (options.json) {
|
|
709
|
-
json(
|
|
1573
|
+
json(it);
|
|
710
1574
|
return;
|
|
711
1575
|
}
|
|
712
|
-
|
|
713
|
-
keyValue("
|
|
714
|
-
keyValue("
|
|
1576
|
+
keyValue("Iteration", it.iterationName);
|
|
1577
|
+
keyValue("Status", formatStatus(it.status));
|
|
1578
|
+
keyValue("Acquired as", it.lastAcquireMode || "(unknown)");
|
|
1579
|
+
keyValue("Run ID", it.runId);
|
|
1580
|
+
keyValue("Instance ID", it.instanceId);
|
|
1581
|
+
keyValue("Started", formatDate(it.startedAt));
|
|
1582
|
+
keyValue("Last run", formatDate(it.lastRunAt));
|
|
1583
|
+
keyValue("Lock expires", formatDate(it.lockExpiresAt));
|
|
1584
|
+
keyValue("Processed", String(it.totalProcessed ?? 0));
|
|
1585
|
+
keyValue("Succeeded", String(it.succeededCount ?? 0));
|
|
1586
|
+
keyValue("Failed (total)", String(it.failedCount ?? 0));
|
|
1587
|
+
keyValue("Skipped", String(it.skippedCount ?? 0));
|
|
1588
|
+
if (it.lastError) {
|
|
1589
|
+
keyValue("Last error", it.lastError);
|
|
1590
|
+
}
|
|
1591
|
+
const sample = Array.isArray(it.failedUserIdsSample)
|
|
1592
|
+
? it.failedUserIdsSample
|
|
1593
|
+
: [];
|
|
1594
|
+
if (sample.length > 0) {
|
|
1595
|
+
const label = it.failedUserIdsTruncated
|
|
1596
|
+
? `Failed user IDs (sample, truncated; ${it.failedCount} total)`
|
|
1597
|
+
: `Failed user IDs (sample)`;
|
|
1598
|
+
keyValue(label, sample.join(", "));
|
|
1599
|
+
}
|
|
715
1600
|
}
|
|
716
1601
|
catch (err) {
|
|
717
1602
|
error(err.message);
|
|
718
1603
|
process.exit(1);
|
|
719
1604
|
}
|
|
720
1605
|
});
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
.
|
|
724
|
-
.
|
|
725
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
726
|
-
.argument("<config-id>", "Configuration ID")
|
|
1606
|
+
iterations
|
|
1607
|
+
.command("reset")
|
|
1608
|
+
.description("Reset a terminal (completed/failed) iteration so the next trigger runs fresh")
|
|
1609
|
+
.argument("<name>", "Iteration name")
|
|
727
1610
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
728
1611
|
.option("--json", "Output as JSON")
|
|
729
|
-
.action(async (
|
|
1612
|
+
.action(async (name, options) => {
|
|
730
1613
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
731
1614
|
const client = new ApiClient();
|
|
732
1615
|
try {
|
|
733
|
-
const
|
|
1616
|
+
const res = await client.resetIteration(resolvedAppId, name);
|
|
734
1617
|
if (options.json) {
|
|
735
|
-
json(
|
|
1618
|
+
json(res);
|
|
736
1619
|
return;
|
|
737
1620
|
}
|
|
738
|
-
success("
|
|
739
|
-
keyValue("Active Config ID", result.activeConfigId || configId);
|
|
1621
|
+
success(`Reset iteration "${name}" (was ${res.previousStatus}). The next trigger will run fresh.`);
|
|
740
1622
|
}
|
|
741
1623
|
catch (err) {
|
|
1624
|
+
// A running iteration is refused with 409 + lock metadata — surface a
|
|
1625
|
+
// clear message rather than a raw error.
|
|
742
1626
|
error(err.message);
|
|
743
1627
|
process.exit(1);
|
|
744
1628
|
}
|
|
745
1629
|
});
|
|
746
|
-
//
|
|
1630
|
+
// ============================================
|
|
1631
|
+
// CONFIGS SUBCOMMAND
|
|
1632
|
+
// ============================================
|
|
1633
|
+
const configs = workflows
|
|
1634
|
+
.command("configs")
|
|
1635
|
+
.description("Manage workflow configurations")
|
|
1636
|
+
.addHelpText("after", `
|
|
1637
|
+
Examples:
|
|
1638
|
+
$ primitive workflows configs list <workflow-id>
|
|
1639
|
+
$ primitive workflows configs get <workflow-id> <config-id>
|
|
1640
|
+
|
|
1641
|
+
Config bodies are authored as files, not verbs: write
|
|
1642
|
+
workflows/<key>.configs/<name>.toml and name the live one with
|
|
1643
|
+
activeConfigName in workflows/<key>.toml, then run 'primitive config push'.
|
|
1644
|
+
`);
|
|
1645
|
+
// List configs
|
|
747
1646
|
configs
|
|
748
|
-
.command("
|
|
749
|
-
.description("
|
|
750
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
751
|
-
.argument("<config-id>", "Configuration ID to duplicate")
|
|
1647
|
+
.command("list")
|
|
1648
|
+
.description("List configurations for a workflow")
|
|
1649
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
752
1650
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
753
|
-
.option("--name <name>", "Name for the new configuration")
|
|
754
1651
|
.option("--json", "Output as JSON")
|
|
755
|
-
.action(async (workflowId,
|
|
1652
|
+
.action(async (workflowId, options) => {
|
|
756
1653
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
757
1654
|
const client = new ApiClient();
|
|
758
1655
|
try {
|
|
759
|
-
const
|
|
760
|
-
configName: options.name,
|
|
761
|
-
});
|
|
1656
|
+
const { items } = await client.listWorkflowConfigs(resolvedAppId, workflowId);
|
|
762
1657
|
if (options.json) {
|
|
763
|
-
json(
|
|
1658
|
+
json(wholeListEnvelope(items));
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (!items || items.length === 0) {
|
|
1662
|
+
info("No configurations found.");
|
|
764
1663
|
return;
|
|
765
1664
|
}
|
|
766
|
-
|
|
767
|
-
|
|
1665
|
+
// Get workflow to find active config
|
|
1666
|
+
const { workflow } = await client.getWorkflow(resolvedAppId, workflowId);
|
|
1667
|
+
const activeConfigId = workflow?.activeConfigId;
|
|
1668
|
+
console.log(formatTable(items, [
|
|
1669
|
+
{ header: "ID", key: "configId", format: formatId },
|
|
1670
|
+
{ header: "NAME", key: "configName" },
|
|
1671
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
1672
|
+
{
|
|
1673
|
+
header: "ACTIVE",
|
|
1674
|
+
key: "configId",
|
|
1675
|
+
format: (v) => v === activeConfigId ? "★" : "",
|
|
1676
|
+
},
|
|
1677
|
+
{ header: "MODIFIED", key: "modifiedAt", format: formatDate },
|
|
1678
|
+
]));
|
|
768
1679
|
}
|
|
769
1680
|
catch (err) {
|
|
770
1681
|
error(err.message);
|
|
771
1682
|
process.exit(1);
|
|
772
1683
|
}
|
|
773
1684
|
});
|
|
774
|
-
//
|
|
1685
|
+
// Get config
|
|
775
1686
|
configs
|
|
776
|
-
.command("
|
|
777
|
-
.description("
|
|
778
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1687
|
+
.command("get")
|
|
1688
|
+
.description("Get configuration details")
|
|
1689
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
779
1690
|
.argument("<config-id>", "Configuration ID")
|
|
780
1691
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
781
|
-
.option("
|
|
1692
|
+
.option("--json", "Output as JSON")
|
|
782
1693
|
.action(async (workflowId, configId, options) => {
|
|
783
1694
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
784
|
-
if (!options.yes) {
|
|
785
|
-
const inquirer = await import("inquirer");
|
|
786
|
-
const { confirm } = await inquirer.default.prompt([
|
|
787
|
-
{
|
|
788
|
-
type: "confirm",
|
|
789
|
-
name: "confirm",
|
|
790
|
-
message: `Are you sure you want to archive configuration ${configId}?`,
|
|
791
|
-
default: false,
|
|
792
|
-
},
|
|
793
|
-
]);
|
|
794
|
-
if (!confirm) {
|
|
795
|
-
info("Cancelled.");
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
1695
|
const client = new ApiClient();
|
|
800
1696
|
try {
|
|
801
|
-
await client.
|
|
802
|
-
|
|
1697
|
+
const config = await client.getWorkflowConfig(resolvedAppId, workflowId, configId);
|
|
1698
|
+
if (options.json) {
|
|
1699
|
+
json(config);
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
printResult("Config ID", config.configId);
|
|
1703
|
+
printResult("Name", config.configName);
|
|
1704
|
+
printResult("Description", config.description || "-");
|
|
1705
|
+
printResult("Status", formatStatus(config.status));
|
|
1706
|
+
printResult("Created", formatDate(config.createdAt));
|
|
1707
|
+
printResult("Modified", formatDate(config.modifiedAt));
|
|
1708
|
+
if (config.steps && config.steps.length > 0) {
|
|
1709
|
+
divider();
|
|
1710
|
+
info(`Steps (${config.steps.length}):`);
|
|
1711
|
+
config.steps.forEach((step, idx) => {
|
|
1712
|
+
console.log(` ${idx + 1}. ${step.id} (${step.kind})`);
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
803
1715
|
}
|
|
804
1716
|
catch (err) {
|
|
805
1717
|
error(err.message);
|
|
@@ -811,33 +1723,50 @@ Examples:
|
|
|
811
1723
|
// ============================================
|
|
812
1724
|
const tests = workflows
|
|
813
1725
|
.command("tests")
|
|
814
|
-
.description("
|
|
1726
|
+
.description("Run and inspect workflow test cases")
|
|
815
1727
|
.addHelpText("after", `
|
|
1728
|
+
Test cases are authored in TOML at workflows/<key>.tests/<name>.toml and applied
|
|
1729
|
+
with \`primitive config push\` (issue #2769); \`config fields workflow\` lists the
|
|
1730
|
+
fields. Deleting a case is removing its file and running \`config push --prune\`.
|
|
1731
|
+
|
|
1732
|
+
Every verb here takes the workflow's KEY or its ID (#3249). An identifier
|
|
1733
|
+
that names no workflow in the app fails; "No test cases found." only ever
|
|
1734
|
+
means the workflow exists and has no cases.
|
|
1735
|
+
|
|
816
1736
|
Examples:
|
|
817
|
-
$ primitive workflows tests list <workflow
|
|
818
|
-
$ primitive workflows tests
|
|
819
|
-
$ primitive workflows tests run <workflow
|
|
820
|
-
$ primitive workflows tests run-all <workflow-
|
|
821
|
-
$ primitive workflows tests run-all <workflow-id> --test-cases 01ABC,01DEF,01GHI
|
|
1737
|
+
$ primitive workflows tests list <workflow>
|
|
1738
|
+
$ primitive workflows tests run <workflow> <test-case-id>
|
|
1739
|
+
$ primitive workflows tests run-all <workflow>
|
|
1740
|
+
$ primitive workflows tests run-all <workflow> --test-cases 01ABC,01DEF,01GHI
|
|
822
1741
|
`);
|
|
823
|
-
// List test cases
|
|
824
1742
|
tests
|
|
825
1743
|
.command("list")
|
|
826
1744
|
.description("List test cases for a workflow")
|
|
827
|
-
.argument("<workflow
|
|
1745
|
+
.argument("<workflow>", "Workflow key or ID")
|
|
828
1746
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1747
|
+
.addOption(pageLimitOption())
|
|
1748
|
+
.addOption(pageCursorOption())
|
|
829
1749
|
.option("--json", "Output as JSON")
|
|
830
|
-
.action(async (
|
|
1750
|
+
.action(async (workflow, options) => {
|
|
1751
|
+
const limit = parsePageLimit(options.limit);
|
|
831
1752
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
832
1753
|
const client = new ApiClient();
|
|
833
1754
|
try {
|
|
834
|
-
|
|
1755
|
+
// #3249 — resolve BEFORE listing: the test-case endpoint answers an
|
|
1756
|
+
// unknown block with an empty page, so an unresolved identifier would
|
|
1757
|
+
// print "No test cases found." and exit 0.
|
|
1758
|
+
const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
|
|
1759
|
+
const page = normalizeCliListEnvelope(await client.listTestCases(resolvedAppId, "workflow", workflowId, {
|
|
1760
|
+
limit,
|
|
1761
|
+
cursor: options.cursor,
|
|
1762
|
+
}));
|
|
1763
|
+
const items = page.items;
|
|
835
1764
|
if (options.json) {
|
|
836
|
-
json(
|
|
1765
|
+
json(page);
|
|
837
1766
|
return;
|
|
838
1767
|
}
|
|
839
|
-
if (
|
|
840
|
-
|
|
1768
|
+
if (items.length === 0) {
|
|
1769
|
+
printEmptyPage(page, "test cases");
|
|
841
1770
|
return;
|
|
842
1771
|
}
|
|
843
1772
|
console.log(formatTable(items, [
|
|
@@ -845,73 +1774,7 @@ Examples:
|
|
|
845
1774
|
{ header: "NAME", key: "name" },
|
|
846
1775
|
{ header: "CREATED", key: "createdAt", format: formatDate },
|
|
847
1776
|
]));
|
|
848
|
-
|
|
849
|
-
catch (err) {
|
|
850
|
-
error(err.message);
|
|
851
|
-
process.exit(1);
|
|
852
|
-
}
|
|
853
|
-
});
|
|
854
|
-
// Create test case
|
|
855
|
-
tests
|
|
856
|
-
.command("create")
|
|
857
|
-
.description("Create a test case for a workflow")
|
|
858
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
859
|
-
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
860
|
-
.option("--name <name>", "Test case name (required)")
|
|
861
|
-
.option("--vars <json>", "Input variables as JSON (required)")
|
|
862
|
-
.option("--pattern <regex>", "Expected output pattern (regex)")
|
|
863
|
-
.option("--contains <json>", "Expected strings to contain (JSON array)")
|
|
864
|
-
.option("--json-subset <json>", "Expected JSON subset to match")
|
|
865
|
-
.option("--json", "Output as JSON")
|
|
866
|
-
.action(async (workflowId, options) => {
|
|
867
|
-
const resolvedAppId = resolveAppId(undefined, options);
|
|
868
|
-
if (!options.name || !options.vars) {
|
|
869
|
-
error("Required: --name and --vars");
|
|
870
|
-
process.exit(1);
|
|
871
|
-
}
|
|
872
|
-
let inputVariables;
|
|
873
|
-
try {
|
|
874
|
-
inputVariables = JSON.parse(options.vars);
|
|
875
|
-
}
|
|
876
|
-
catch {
|
|
877
|
-
error("Invalid JSON in --vars");
|
|
878
|
-
process.exit(1);
|
|
879
|
-
}
|
|
880
|
-
let expectedOutputContains;
|
|
881
|
-
if (options.contains) {
|
|
882
|
-
try {
|
|
883
|
-
expectedOutputContains = JSON.parse(options.contains);
|
|
884
|
-
}
|
|
885
|
-
catch {
|
|
886
|
-
error("Invalid JSON in --contains");
|
|
887
|
-
process.exit(1);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
let expectedJsonSubset;
|
|
891
|
-
if (options.jsonSubset) {
|
|
892
|
-
try {
|
|
893
|
-
expectedJsonSubset = JSON.parse(options.jsonSubset);
|
|
894
|
-
}
|
|
895
|
-
catch {
|
|
896
|
-
error("Invalid JSON in --json-subset");
|
|
897
|
-
process.exit(1);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
const client = new ApiClient();
|
|
901
|
-
try {
|
|
902
|
-
const result = await client.createTestCase(resolvedAppId, "workflow", workflowId, {
|
|
903
|
-
name: options.name,
|
|
904
|
-
inputVariables,
|
|
905
|
-
expectedOutputPattern: options.pattern,
|
|
906
|
-
expectedOutputContains,
|
|
907
|
-
expectedJsonSubset,
|
|
908
|
-
});
|
|
909
|
-
if (options.json) {
|
|
910
|
-
json(result);
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
success(`Test case created: ${result.name}`);
|
|
914
|
-
keyValue("Test Case ID", result.testCaseId);
|
|
1777
|
+
printPageHint(page);
|
|
915
1778
|
}
|
|
916
1779
|
catch (err) {
|
|
917
1780
|
error(err.message);
|
|
@@ -922,26 +1785,29 @@ Examples:
|
|
|
922
1785
|
tests
|
|
923
1786
|
.command("get")
|
|
924
1787
|
.description("Get test case details")
|
|
925
|
-
.argument("<workflow
|
|
1788
|
+
.argument("<workflow>", "Workflow key or ID")
|
|
926
1789
|
.argument("<test-case-id>", "Test Case ID")
|
|
927
1790
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
928
1791
|
.option("--json", "Output as JSON")
|
|
929
|
-
.action(async (
|
|
1792
|
+
.action(async (workflow, testCaseId, options) => {
|
|
930
1793
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
931
1794
|
const client = new ApiClient();
|
|
932
1795
|
try {
|
|
1796
|
+
const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
|
|
933
1797
|
const result = await client.getTestCase(resolvedAppId, "workflow", workflowId, testCaseId);
|
|
934
1798
|
if (options.json) {
|
|
935
1799
|
json(result);
|
|
936
1800
|
return;
|
|
937
1801
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1802
|
+
printResult("Test Case ID", result.testCaseId);
|
|
1803
|
+
printResult("Name", result.name);
|
|
1804
|
+
printResult("Created", formatDate(result.createdAt));
|
|
941
1805
|
divider();
|
|
942
1806
|
info("Input Variables:");
|
|
943
1807
|
try {
|
|
944
|
-
const vars =
|
|
1808
|
+
const vars = typeof result.inputVariables === 'string'
|
|
1809
|
+
? JSON.parse(result.inputVariables || "{}")
|
|
1810
|
+
: (result.inputVariables || {});
|
|
945
1811
|
console.log(JSON.stringify(vars, null, 2));
|
|
946
1812
|
}
|
|
947
1813
|
catch {
|
|
@@ -949,13 +1815,16 @@ Examples:
|
|
|
949
1815
|
}
|
|
950
1816
|
if (result.expectedOutputPattern) {
|
|
951
1817
|
divider();
|
|
952
|
-
|
|
1818
|
+
printResult("Expected Pattern", result.expectedOutputPattern);
|
|
953
1819
|
}
|
|
954
1820
|
if (result.expectedOutputContains) {
|
|
955
1821
|
divider();
|
|
956
1822
|
info("Expected Contains:");
|
|
957
1823
|
try {
|
|
958
|
-
|
|
1824
|
+
const contains = typeof result.expectedOutputContains === 'string'
|
|
1825
|
+
? JSON.parse(result.expectedOutputContains)
|
|
1826
|
+
: result.expectedOutputContains;
|
|
1827
|
+
console.log(JSON.stringify(contains, null, 2));
|
|
959
1828
|
}
|
|
960
1829
|
catch {
|
|
961
1830
|
console.log(result.expectedOutputContains);
|
|
@@ -965,7 +1834,10 @@ Examples:
|
|
|
965
1834
|
divider();
|
|
966
1835
|
info("Expected JSON Subset:");
|
|
967
1836
|
try {
|
|
968
|
-
|
|
1837
|
+
const subset = typeof result.expectedJsonSubset === 'string'
|
|
1838
|
+
? JSON.parse(result.expectedJsonSubset)
|
|
1839
|
+
: result.expectedJsonSubset;
|
|
1840
|
+
console.log(JSON.stringify(subset, null, 2));
|
|
969
1841
|
}
|
|
970
1842
|
catch {
|
|
971
1843
|
console.log(result.expectedJsonSubset);
|
|
@@ -977,140 +1849,25 @@ Examples:
|
|
|
977
1849
|
process.exit(1);
|
|
978
1850
|
}
|
|
979
1851
|
});
|
|
980
|
-
// Update test case
|
|
981
|
-
tests
|
|
982
|
-
.command("update")
|
|
983
|
-
.description("Update a test case")
|
|
984
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
985
|
-
.argument("<test-case-id>", "Test Case ID")
|
|
986
|
-
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
987
|
-
.option("--name <name>", "Test case name")
|
|
988
|
-
.option("--vars <json>", "Input variables as JSON")
|
|
989
|
-
.option("--pattern <regex>", "Expected output pattern (regex)")
|
|
990
|
-
.option("--contains <json>", "Expected strings to contain (JSON array)")
|
|
991
|
-
.option("--json-subset <json>", "Expected JSON subset to match")
|
|
992
|
-
.option("--clear-pattern", "Clear expected pattern")
|
|
993
|
-
.option("--clear-contains", "Clear expected contains")
|
|
994
|
-
.option("--clear-json-subset", "Clear expected JSON subset")
|
|
995
|
-
.option("--json", "Output as JSON")
|
|
996
|
-
.action(async (workflowId, testCaseId, options) => {
|
|
997
|
-
const resolvedAppId = resolveAppId(undefined, options);
|
|
998
|
-
const payload = {};
|
|
999
|
-
if (options.name)
|
|
1000
|
-
payload.name = options.name;
|
|
1001
|
-
if (options.vars) {
|
|
1002
|
-
try {
|
|
1003
|
-
payload.inputVariables = JSON.parse(options.vars);
|
|
1004
|
-
}
|
|
1005
|
-
catch {
|
|
1006
|
-
error("Invalid JSON in --vars");
|
|
1007
|
-
process.exit(1);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
if (options.clearPattern) {
|
|
1011
|
-
payload.expectedOutputPattern = null;
|
|
1012
|
-
}
|
|
1013
|
-
else if (options.pattern) {
|
|
1014
|
-
payload.expectedOutputPattern = options.pattern;
|
|
1015
|
-
}
|
|
1016
|
-
if (options.clearContains) {
|
|
1017
|
-
payload.expectedOutputContains = null;
|
|
1018
|
-
}
|
|
1019
|
-
else if (options.contains) {
|
|
1020
|
-
try {
|
|
1021
|
-
payload.expectedOutputContains = JSON.parse(options.contains);
|
|
1022
|
-
}
|
|
1023
|
-
catch {
|
|
1024
|
-
error("Invalid JSON in --contains");
|
|
1025
|
-
process.exit(1);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
if (options.clearJsonSubset) {
|
|
1029
|
-
payload.expectedJsonSubset = null;
|
|
1030
|
-
}
|
|
1031
|
-
else if (options.jsonSubset) {
|
|
1032
|
-
try {
|
|
1033
|
-
payload.expectedJsonSubset = JSON.parse(options.jsonSubset);
|
|
1034
|
-
}
|
|
1035
|
-
catch {
|
|
1036
|
-
error("Invalid JSON in --json-subset");
|
|
1037
|
-
process.exit(1);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
if (Object.keys(payload).length === 0) {
|
|
1041
|
-
error("No update options specified.");
|
|
1042
|
-
process.exit(1);
|
|
1043
|
-
}
|
|
1044
|
-
const client = new ApiClient();
|
|
1045
|
-
try {
|
|
1046
|
-
const result = await client.updateTestCase(resolvedAppId, "workflow", workflowId, testCaseId, payload);
|
|
1047
|
-
if (options.json) {
|
|
1048
|
-
json(result);
|
|
1049
|
-
return;
|
|
1050
|
-
}
|
|
1051
|
-
success("Test case updated.");
|
|
1052
|
-
}
|
|
1053
|
-
catch (err) {
|
|
1054
|
-
error(err.message);
|
|
1055
|
-
process.exit(1);
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
1058
|
-
// Delete test case
|
|
1059
|
-
tests
|
|
1060
|
-
.command("delete")
|
|
1061
|
-
.description("Delete a test case")
|
|
1062
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1063
|
-
.argument("<test-case-id>", "Test Case ID")
|
|
1064
|
-
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1065
|
-
.option("-y, --yes", "Skip confirmation prompt")
|
|
1066
|
-
.action(async (workflowId, testCaseId, options) => {
|
|
1067
|
-
const resolvedAppId = resolveAppId(undefined, options);
|
|
1068
|
-
if (!options.yes) {
|
|
1069
|
-
const inquirer = await import("inquirer");
|
|
1070
|
-
const { confirm } = await inquirer.default.prompt([
|
|
1071
|
-
{
|
|
1072
|
-
type: "confirm",
|
|
1073
|
-
name: "confirm",
|
|
1074
|
-
message: `Are you sure you want to delete test case ${testCaseId}?`,
|
|
1075
|
-
default: false,
|
|
1076
|
-
},
|
|
1077
|
-
]);
|
|
1078
|
-
if (!confirm) {
|
|
1079
|
-
info("Cancelled.");
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
const client = new ApiClient();
|
|
1084
|
-
try {
|
|
1085
|
-
await client.deleteTestCase(resolvedAppId, "workflow", workflowId, testCaseId);
|
|
1086
|
-
success("Test case deleted.");
|
|
1087
|
-
}
|
|
1088
|
-
catch (err) {
|
|
1089
|
-
error(err.message);
|
|
1090
|
-
process.exit(1);
|
|
1091
|
-
}
|
|
1092
|
-
});
|
|
1093
1852
|
// Run a single test case
|
|
1094
1853
|
tests
|
|
1095
1854
|
.command("run")
|
|
1096
1855
|
.description("Run a single test case")
|
|
1097
|
-
.argument("<workflow
|
|
1856
|
+
.argument("<workflow>", "Workflow key or ID")
|
|
1098
1857
|
.argument("<test-case-id>", "Test Case ID")
|
|
1099
1858
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1100
1859
|
.option("--json", "Output as JSON")
|
|
1101
|
-
.action(async (
|
|
1860
|
+
.action(async (workflow, testCaseId, options) => {
|
|
1102
1861
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1103
1862
|
const client = new ApiClient();
|
|
1104
1863
|
try {
|
|
1864
|
+
const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
|
|
1105
1865
|
// Get test case to retrieve its variables
|
|
1106
1866
|
const testCase = await client.getTestCase(resolvedAppId, "workflow", workflowId, testCaseId);
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
catch {
|
|
1112
|
-
// Use empty object if parsing fails
|
|
1113
|
-
}
|
|
1867
|
+
// #3158 — the case's stored input IS the run's input, and the server
|
|
1868
|
+
// hands it back parsed; `JSON.parse` on that object threw and the run
|
|
1869
|
+
// went ahead with `{}`, disagreeing with `run-all` over the same case.
|
|
1870
|
+
const variables = requireTestCaseInputVariables(testCase.inputVariables, testCaseId);
|
|
1114
1871
|
const result = await client.executeBlockTest(resolvedAppId, "workflow", workflowId, {
|
|
1115
1872
|
variables,
|
|
1116
1873
|
testCaseId,
|
|
@@ -1163,14 +1920,17 @@ Examples:
|
|
|
1163
1920
|
tests
|
|
1164
1921
|
.command("run-all")
|
|
1165
1922
|
.description("Run all test cases for a workflow, or a specific subset")
|
|
1166
|
-
.argument("<workflow
|
|
1923
|
+
.argument("<workflow>", "Workflow key or ID")
|
|
1167
1924
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1168
1925
|
.option("--test-cases <ids>", "Comma-separated list of test case IDs to run (runs only these)")
|
|
1169
1926
|
.option("--json", "Output as JSON")
|
|
1170
|
-
.action(async (
|
|
1927
|
+
.action(async (workflow, options) => {
|
|
1171
1928
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1172
1929
|
const client = new ApiClient();
|
|
1173
1930
|
try {
|
|
1931
|
+
// #3249 — resolve first, so `Total Tests: 0` can only ever mean the
|
|
1932
|
+
// workflow exists and has no cases, never that the identifier was wrong.
|
|
1933
|
+
const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
|
|
1174
1934
|
// Parse test case IDs if provided
|
|
1175
1935
|
let testCaseIds;
|
|
1176
1936
|
if (options.testCases) {
|
|
@@ -1227,15 +1987,16 @@ Examples:
|
|
|
1227
1987
|
tests
|
|
1228
1988
|
.command("runs")
|
|
1229
1989
|
.description("List recent test runs for a workflow")
|
|
1230
|
-
.argument("<workflow
|
|
1990
|
+
.argument("<workflow>", "Workflow key or ID")
|
|
1231
1991
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1232
1992
|
.option("--limit <n>", "Maximum number of runs to show", "20")
|
|
1233
1993
|
.option("--group <group>", "Filter by comparison group")
|
|
1234
1994
|
.option("--json", "Output as JSON")
|
|
1235
|
-
.action(async (
|
|
1995
|
+
.action(async (workflow, options) => {
|
|
1236
1996
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1237
1997
|
const client = new ApiClient();
|
|
1238
1998
|
try {
|
|
1999
|
+
const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
|
|
1239
2000
|
const { items } = await client.listTestRuns(resolvedAppId, "workflow", workflowId, {
|
|
1240
2001
|
limit: parseInt(options.limit),
|
|
1241
2002
|
comparisonGroup: options.group,
|
|
@@ -1277,23 +2038,24 @@ Examples:
|
|
|
1277
2038
|
.description("Run tests in parallel using workflow-based execution")
|
|
1278
2039
|
.addHelpText("after", `
|
|
1279
2040
|
Examples:
|
|
1280
|
-
$ primitive workflows tests batch start <workflow
|
|
1281
|
-
$ primitive workflows tests batch start <workflow
|
|
1282
|
-
$ primitive workflows tests batch status <workflow
|
|
1283
|
-
$ primitive workflows tests batch cancel <workflow
|
|
2041
|
+
$ primitive workflows tests batch start <workflow>
|
|
2042
|
+
$ primitive workflows tests batch start <workflow> --test-cases 01ABC,01DEF
|
|
2043
|
+
$ primitive workflows tests batch status <workflow> <batch-id>
|
|
2044
|
+
$ primitive workflows tests batch cancel <workflow> <batch-id>
|
|
1284
2045
|
`);
|
|
1285
2046
|
// Start batch tests
|
|
1286
2047
|
batch
|
|
1287
2048
|
.command("start")
|
|
1288
2049
|
.description("Start parallel batch test execution")
|
|
1289
|
-
.argument("<workflow
|
|
2050
|
+
.argument("<workflow>", "Workflow key or ID")
|
|
1290
2051
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1291
2052
|
.option("--test-cases <ids>", "Comma-separated list of test case IDs to run")
|
|
1292
2053
|
.option("--json", "Output as JSON")
|
|
1293
|
-
.action(async (
|
|
2054
|
+
.action(async (workflow, options) => {
|
|
1294
2055
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1295
2056
|
const client = new ApiClient();
|
|
1296
2057
|
try {
|
|
2058
|
+
const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
|
|
1297
2059
|
let testCaseIds;
|
|
1298
2060
|
if (options.testCases) {
|
|
1299
2061
|
testCaseIds = options.testCases.split(",").map((id) => id.trim()).filter(Boolean);
|
|
@@ -1329,27 +2091,37 @@ Examples:
|
|
|
1329
2091
|
batch
|
|
1330
2092
|
.command("status")
|
|
1331
2093
|
.description("Get status of a batch test execution")
|
|
1332
|
-
.argument("<workflow
|
|
2094
|
+
.argument("<workflow>", "Workflow key or ID")
|
|
1333
2095
|
.argument("<batch-id>", "Batch ID")
|
|
1334
2096
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1335
2097
|
.option("--wait", "Wait for completion (polls every 2 seconds)")
|
|
1336
2098
|
.option("--json", "Output as JSON")
|
|
1337
|
-
.action(async (
|
|
2099
|
+
.action(async (workflow, batchId, options) => {
|
|
1338
2100
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1339
2101
|
const client = new ApiClient();
|
|
1340
2102
|
try {
|
|
2103
|
+
const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
|
|
1341
2104
|
const fetchStatus = async () => {
|
|
1342
2105
|
return client.getBatchTestStatus(resolvedAppId, "workflow", workflowId, batchId);
|
|
1343
2106
|
};
|
|
1344
2107
|
let result = await fetchStatus();
|
|
1345
2108
|
if (options.wait && result.status === "running") {
|
|
1346
2109
|
info("Waiting for batch completion...");
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
2110
|
+
// #1969 — shared poll primitive (delay-then-probe every 2s, honoring
|
|
2111
|
+
// an AbortSignal). `onError: "throw"` preserves the prior loop's
|
|
2112
|
+
// fail-fast behavior: a throwing fetchStatus (expired token, deleted
|
|
2113
|
+
// batch → 401/404) propagates to the command-level catch below and
|
|
2114
|
+
// exits non-zero, rather than spinning forever with a frozen progress
|
|
2115
|
+
// line. (The follow/watch tail loops legitimately keep "continue".)
|
|
2116
|
+
const controller = new AbortController();
|
|
2117
|
+
result = await pollUntil(fetchStatus, (r) => r.status !== "running", {
|
|
2118
|
+
intervalMs: 2000,
|
|
2119
|
+
signal: controller.signal,
|
|
2120
|
+
onError: "throw",
|
|
2121
|
+
// Progress goes to stderr so it can't corrupt JSON on stdout under --json.
|
|
2122
|
+
onValue: (r) => progress(` Completed: ${r.completed}/${r.results?.length || 0} `),
|
|
2123
|
+
});
|
|
2124
|
+
progressEnd();
|
|
1353
2125
|
}
|
|
1354
2126
|
if (options.json) {
|
|
1355
2127
|
json(result);
|
|
@@ -1385,23 +2157,22 @@ Examples:
|
|
|
1385
2157
|
batch
|
|
1386
2158
|
.command("cancel")
|
|
1387
2159
|
.description("Cancel a running batch test execution")
|
|
1388
|
-
.argument("<workflow
|
|
2160
|
+
.argument("<workflow>", "Workflow key or ID")
|
|
1389
2161
|
.argument("<batch-id>", "Batch ID")
|
|
1390
2162
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1391
2163
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
1392
2164
|
.option("--json", "Output as JSON")
|
|
1393
|
-
.action(async (
|
|
2165
|
+
.action(async (workflow, batchId, options) => {
|
|
1394
2166
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1395
2167
|
if (!options.yes) {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
{
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
]);
|
|
2168
|
+
let confirm;
|
|
2169
|
+
try {
|
|
2170
|
+
confirm = await confirmPrompt(`Are you sure you want to cancel batch ${batchId}?`);
|
|
2171
|
+
}
|
|
2172
|
+
catch (err) {
|
|
2173
|
+
error(err.message);
|
|
2174
|
+
process.exit(1);
|
|
2175
|
+
}
|
|
1405
2176
|
if (!confirm) {
|
|
1406
2177
|
info("Cancelled.");
|
|
1407
2178
|
return;
|
|
@@ -1409,6 +2180,7 @@ Examples:
|
|
|
1409
2180
|
}
|
|
1410
2181
|
const client = new ApiClient();
|
|
1411
2182
|
try {
|
|
2183
|
+
const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
|
|
1412
2184
|
// First get current status to get instance IDs
|
|
1413
2185
|
const status = await client.getBatchTestStatus(resolvedAppId, "workflow", workflowId, batchId);
|
|
1414
2186
|
// Get instance IDs that are still running
|
|
@@ -1441,29 +2213,32 @@ Examples:
|
|
|
1441
2213
|
// ============================================
|
|
1442
2214
|
const attachments = tests
|
|
1443
2215
|
.command("attachments")
|
|
1444
|
-
.description("
|
|
2216
|
+
.description("Inspect test case file attachments")
|
|
1445
2217
|
.addHelpText("after", `
|
|
2218
|
+
Attachments are authored as files in workflows/<key>.tests/<name>/ and applied
|
|
2219
|
+
with \`primitive config push\` (#2769); removing one and running
|
|
2220
|
+
\`config push --prune\` deletes it server-side. These verbs read server state.
|
|
2221
|
+
|
|
1446
2222
|
Examples:
|
|
1447
|
-
$ primitive workflows tests attachments list <workflow
|
|
1448
|
-
$ primitive workflows tests attachments
|
|
1449
|
-
$ primitive workflows tests attachments download <workflow-id> <test-case-id> document.pdf
|
|
1450
|
-
$ primitive workflows tests attachments delete <workflow-id> <test-case-id> document.pdf
|
|
2223
|
+
$ primitive workflows tests attachments list <workflow> <test-case-id>
|
|
2224
|
+
$ primitive workflows tests attachments download <workflow> <test-case-id> document.pdf
|
|
1451
2225
|
`);
|
|
1452
2226
|
// List attachments
|
|
1453
2227
|
attachments
|
|
1454
2228
|
.command("list")
|
|
1455
2229
|
.description("List attachments for a test case")
|
|
1456
|
-
.argument("<workflow
|
|
2230
|
+
.argument("<workflow>", "Workflow key or ID")
|
|
1457
2231
|
.argument("<test-case-id>", "Test Case ID")
|
|
1458
2232
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1459
2233
|
.option("--json", "Output as JSON")
|
|
1460
|
-
.action(async (
|
|
2234
|
+
.action(async (workflow, testCaseId, options) => {
|
|
1461
2235
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1462
2236
|
const client = new ApiClient();
|
|
1463
2237
|
try {
|
|
2238
|
+
const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
|
|
1464
2239
|
const result = await client.listTestCaseAttachments(resolvedAppId, "workflow", workflowId, testCaseId);
|
|
1465
2240
|
if (options.json) {
|
|
1466
|
-
json(result.attachments);
|
|
2241
|
+
json(wholeListEnvelope(result.attachments));
|
|
1467
2242
|
return;
|
|
1468
2243
|
}
|
|
1469
2244
|
if (!result.attachments || result.attachments.length === 0) {
|
|
@@ -1485,62 +2260,20 @@ Examples:
|
|
|
1485
2260
|
process.exit(1);
|
|
1486
2261
|
}
|
|
1487
2262
|
});
|
|
1488
|
-
// Upload attachment
|
|
1489
|
-
attachments
|
|
1490
|
-
.command("upload")
|
|
1491
|
-
.description("Upload a file attachment to a test case")
|
|
1492
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1493
|
-
.argument("<test-case-id>", "Test Case ID")
|
|
1494
|
-
.argument("<file-path>", "Path to the file to upload")
|
|
1495
|
-
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1496
|
-
.option("--name <name>", "Override the filename")
|
|
1497
|
-
.option("--json", "Output as JSON")
|
|
1498
|
-
.action(async (workflowId, testCaseId, filePath, options) => {
|
|
1499
|
-
const resolvedAppId = resolveAppId(undefined, options);
|
|
1500
|
-
const client = new ApiClient();
|
|
1501
|
-
try {
|
|
1502
|
-
// Check file exists and get stats
|
|
1503
|
-
const stats = statSync(filePath);
|
|
1504
|
-
if (!stats.isFile()) {
|
|
1505
|
-
error(`Not a file: ${filePath}`);
|
|
1506
|
-
process.exit(1);
|
|
1507
|
-
}
|
|
1508
|
-
// Check file size (10 MB limit)
|
|
1509
|
-
const TEN_MB = 10 * 1024 * 1024;
|
|
1510
|
-
if (stats.size > TEN_MB) {
|
|
1511
|
-
error(`File too large (max 10MB): ${formatFileSize(stats.size)}`);
|
|
1512
|
-
process.exit(1);
|
|
1513
|
-
}
|
|
1514
|
-
const filename = options.name || basename(filePath);
|
|
1515
|
-
const data = readFileSync(filePath);
|
|
1516
|
-
const contentType = mimeLookup(filePath) || "application/octet-stream";
|
|
1517
|
-
const result = await client.uploadTestCaseAttachment(resolvedAppId, "workflow", workflowId, testCaseId, filename, data, contentType);
|
|
1518
|
-
if (options.json) {
|
|
1519
|
-
json(result);
|
|
1520
|
-
return;
|
|
1521
|
-
}
|
|
1522
|
-
success(`Uploaded: ${filename}`);
|
|
1523
|
-
keyValue("Size", formatFileSize(result.attachment.size));
|
|
1524
|
-
keyValue("Type", result.attachment.contentType);
|
|
1525
|
-
}
|
|
1526
|
-
catch (err) {
|
|
1527
|
-
error(err.message);
|
|
1528
|
-
process.exit(1);
|
|
1529
|
-
}
|
|
1530
|
-
});
|
|
1531
2263
|
// Download attachment
|
|
1532
2264
|
attachments
|
|
1533
2265
|
.command("download")
|
|
1534
2266
|
.description("Download a file attachment from a test case")
|
|
1535
|
-
.argument("<workflow
|
|
2267
|
+
.argument("<workflow>", "Workflow key or ID")
|
|
1536
2268
|
.argument("<test-case-id>", "Test Case ID")
|
|
1537
2269
|
.argument("<filename>", "Name of the file to download")
|
|
1538
2270
|
.argument("[output-path]", "Output path (defaults to current directory)")
|
|
1539
2271
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1540
|
-
.action(async (
|
|
2272
|
+
.action(async (workflow, testCaseId, filename, outputPath, options) => {
|
|
1541
2273
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1542
2274
|
const client = new ApiClient();
|
|
1543
2275
|
try {
|
|
2276
|
+
const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
|
|
1544
2277
|
const result = await client.downloadTestCaseAttachment(resolvedAppId, "workflow", workflowId, testCaseId, filename);
|
|
1545
2278
|
const finalPath = outputPath || filename;
|
|
1546
2279
|
writeFileSync(finalPath, result.data);
|
|
@@ -1553,36 +2286,129 @@ Examples:
|
|
|
1553
2286
|
process.exit(1);
|
|
1554
2287
|
}
|
|
1555
2288
|
});
|
|
1556
|
-
//
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
.
|
|
1561
|
-
.
|
|
1562
|
-
.argument("
|
|
1563
|
-
.option("--
|
|
1564
|
-
.option("
|
|
1565
|
-
.
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
2289
|
+
// ============================================
|
|
2290
|
+
// Codegen command (issue #1442)
|
|
2291
|
+
// ============================================
|
|
2292
|
+
workflows
|
|
2293
|
+
.command("codegen")
|
|
2294
|
+
.description("Generate typed client signatures from the local workflows/*.toml schemas. TypeScript (default): one <key>.generated.ts per workflow (<Key>Input/<Key>Output types + a <key>(client) invoker factory). Swift (--lang swift): one <key>.generated.swift per workflow (<Key>Input/<Key>Output Codable types).\n\nFormatting: the emitted TypeScript is already formatted the way Prettier's DEFAULT config formats it, so a `codegen` -> `prettier --write` -> `codegen --check` pipeline stays green. If your project uses a NON-default Prettier config (a different printWidth, trailingComma, or quote style), Prettier will reformat the generated files and `--check` will then fail: add the generated directory to `.prettierignore` and commit the raw emission instead.")
|
|
2295
|
+
.argument("[workflow-key]", "Generate for a single workflow (defaults to every workflows/*.toml found)")
|
|
2296
|
+
.option("-o, --output <dir>", "Output directory for the generated files (defaults to <config-dir>/workflows/generated/)")
|
|
2297
|
+
.option("--lang <lang>", "Target language for the generated types: 'ts' (default) or 'swift'.", "ts")
|
|
2298
|
+
.option("--check", "Exit non-zero if generated output is out of date (CI guard); does not write. Compares byte-for-byte, so the file on disk must match the emission exactly.")
|
|
2299
|
+
.option("--json", "Output the result summary as JSON")
|
|
2300
|
+
.action(async (workflowKey, options) => {
|
|
2301
|
+
try {
|
|
2302
|
+
// 0. Validate --lang up front so an unknown value fails clearly rather
|
|
2303
|
+
// than silently defaulting to the TS emitter.
|
|
2304
|
+
const lang = String(options.lang ?? "ts").toLowerCase();
|
|
2305
|
+
if (lang !== "ts" && lang !== "swift") {
|
|
2306
|
+
error(`Unknown --lang "${options.lang}". Use "ts" or "swift".`);
|
|
2307
|
+
process.exit(1);
|
|
2308
|
+
}
|
|
2309
|
+
// 1. The single source workflows/ directory: the environment's, and
|
|
2310
|
+
// only ever the environment's (#1510, #3154).
|
|
2311
|
+
const workflowsSourceDir = resolveCodegenSourceDir({
|
|
2312
|
+
subdir: "workflows",
|
|
2313
|
+
});
|
|
2314
|
+
// 2. Collect the source .toml files (one per workflow), filtered to a
|
|
2315
|
+
// single workflow when an argument is given. Match on the file stem;
|
|
2316
|
+
// the generator resolves the real key from `[workflow].key`.
|
|
2317
|
+
const inputs = [];
|
|
2318
|
+
for (const fileName of readdirSync(workflowsSourceDir)) {
|
|
2319
|
+
if (!fileName.endsWith(".toml"))
|
|
2320
|
+
continue;
|
|
2321
|
+
const stem = fileName.slice(0, -".toml".length);
|
|
2322
|
+
if (workflowKey && stem !== workflowKey)
|
|
2323
|
+
continue;
|
|
2324
|
+
const tomlPath = path.join(workflowsSourceDir, fileName);
|
|
2325
|
+
inputs.push({
|
|
2326
|
+
fileStem: stem,
|
|
2327
|
+
tomlPath,
|
|
2328
|
+
tomlContent: readFileSync(tomlPath, "utf-8"),
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
if (inputs.length === 0) {
|
|
2332
|
+
error(workflowKey
|
|
2333
|
+
? `No workflows/${workflowKey}.toml found under primitive/<env>/.`
|
|
2334
|
+
: "No workflows/*.toml files found to generate from.");
|
|
2335
|
+
process.exit(1);
|
|
2336
|
+
}
|
|
2337
|
+
// 3. Resolve the output directory. Default: a `generated/` subdir of
|
|
2338
|
+
// the first source dir (keeps generated output beside the schema).
|
|
2339
|
+
const outputDir = options.output
|
|
2340
|
+
? path.resolve(options.output)
|
|
2341
|
+
: path.join(path.dirname(inputs[0].tomlPath), "generated");
|
|
2342
|
+
// 4. Run codegen (or --check) with the selected emitter. Both the TS
|
|
2343
|
+
// and Swift generators return the same WriteOrCheckResult shape, so
|
|
2344
|
+
// the reporting below is emitter-agnostic. Fork 1 = A: only the
|
|
2345
|
+
// emitter differs; both read the same synced TOMLs and the same
|
|
2346
|
+
// drift-guarded schema descriptor.
|
|
2347
|
+
const codegenResult = lang === "swift"
|
|
2348
|
+
? await generateWorkflowSwiftTypes({
|
|
2349
|
+
inputs,
|
|
2350
|
+
outputDir,
|
|
2351
|
+
check: !!options.check,
|
|
2352
|
+
singleWorkflow: !!workflowKey,
|
|
2353
|
+
})
|
|
2354
|
+
: await generateWorkflowTypes({
|
|
2355
|
+
inputs,
|
|
2356
|
+
outputDir,
|
|
2357
|
+
check: !!options.check,
|
|
2358
|
+
singleWorkflow: !!workflowKey,
|
|
2359
|
+
});
|
|
2360
|
+
if (options.check) {
|
|
2361
|
+
if (codegenResult.mismatches.length > 0) {
|
|
2362
|
+
if (options.json) {
|
|
2363
|
+
json({ ok: false, mismatches: codegenResult.mismatches });
|
|
2364
|
+
}
|
|
2365
|
+
else {
|
|
2366
|
+
error(`Check failed: ${codegenResult.mismatches.length} file(s) out of date.`);
|
|
2367
|
+
for (const m of codegenResult.mismatches) {
|
|
2368
|
+
error(` ${m.reason}: ${path.relative(process.cwd(), m.filePath)}`);
|
|
2369
|
+
}
|
|
2370
|
+
// Echo back the options that decide WHERE and WHAT was
|
|
2371
|
+
// checked. Suggesting a bare `workflows codegen` after a
|
|
2372
|
+
// `--check -o <dir>` sent people to regenerate into the
|
|
2373
|
+
// default <config-dir>/workflows/generated/ instead of the
|
|
2374
|
+
// directory that is actually out of date, so the files stayed
|
|
2375
|
+
// stale and the next check failed the same way.
|
|
2376
|
+
const regenerate = ["primitive workflows codegen"];
|
|
2377
|
+
if (workflowKey)
|
|
2378
|
+
regenerate.push(workflowKey);
|
|
2379
|
+
if (lang === "swift")
|
|
2380
|
+
regenerate.push("--lang swift");
|
|
2381
|
+
if (options.output)
|
|
2382
|
+
regenerate.push(`-o ${options.output}`);
|
|
2383
|
+
info(`Run \`${regenerate.join(" ")}\` to regenerate.`);
|
|
2384
|
+
}
|
|
2385
|
+
process.exit(1);
|
|
2386
|
+
}
|
|
2387
|
+
if (options.json) {
|
|
2388
|
+
json({ ok: true, checked: codegenResult.writtenFiles.length });
|
|
2389
|
+
}
|
|
2390
|
+
else {
|
|
2391
|
+
success(`Check passed: ${codegenResult.writtenFiles.length} file(s) up to date.`);
|
|
2392
|
+
}
|
|
1579
2393
|
return;
|
|
1580
2394
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
2395
|
+
if (options.json) {
|
|
2396
|
+
json({
|
|
2397
|
+
written: codegenResult.writtenFiles,
|
|
2398
|
+
deleted: codegenResult.deletedFiles,
|
|
2399
|
+
});
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
success(`Generated ${codegenResult.writtenFiles.length} file(s)` +
|
|
2403
|
+
(codegenResult.deletedFiles.length > 0
|
|
2404
|
+
? `, deleted ${codegenResult.deletedFiles.length} stale file(s).`
|
|
2405
|
+
: "."));
|
|
2406
|
+
for (const f of codegenResult.writtenFiles) {
|
|
2407
|
+
keyValue(" wrote", path.relative(process.cwd(), f));
|
|
2408
|
+
}
|
|
2409
|
+
for (const f of codegenResult.deletedFiles) {
|
|
2410
|
+
keyValue(" deleted", path.relative(process.cwd(), f));
|
|
2411
|
+
}
|
|
1586
2412
|
}
|
|
1587
2413
|
catch (err) {
|
|
1588
2414
|
error(err.message);
|