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