primitive-admin 1.1.0-alpha.6 → 1.1.0-alpha.60
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 +389 -71
- package/assets/skill/skills/primitive-platform/SKILL.md +670 -0
- package/dist/bin/primitive.d.ts +2 -0
- package/dist/bin/primitive.js +287 -17
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.d.ts +2 -0
- package/dist/src/commands/admins.js +138 -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 +554 -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 +27 -80
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth.d.ts +2 -0
- package/dist/src/commands/auth.js +177 -7
- 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 +92 -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 +526 -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 +35 -0
- package/dist/src/commands/config.js +437 -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 +204 -0
- package/dist/src/commands/cron-triggers.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 +2153 -96
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +2 -0
- package/dist/src/commands/documents.js +1230 -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 +12 -0
- package/dist/src/commands/env.js +260 -0
- package/dist/src/commands/env.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 +38 -99
- 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 +17 -0
- package/dist/src/commands/init.js +964 -202
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.d.ts +2 -0
- package/dist/src/commands/integrations.js +484 -205
- 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.d.ts +2 -0
- package/dist/src/commands/metadata.js +406 -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 +135 -355
- 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 +271 -0
- package/dist/src/commands/rule-sets.js.map +1 -0
- package/dist/src/commands/scripts.d.ts +20 -0
- package/dist/src/commands/scripts.js +739 -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 +125 -0
- package/dist/src/commands/sync-app-settings.js +380 -0
- package/dist/src/commands/sync-app-settings.js.map +1 -0
- package/dist/src/commands/sync.d.ts +958 -0
- package/dist/src/commands/sync.js +8954 -812
- 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 +440 -22
- 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 +442 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.d.ts +94 -0
- package/dist/src/commands/workflows.js +1581 -448
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +1814 -0
- package/dist/src/lib/api-client.js +1655 -103
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +130 -0
- package/dist/src/lib/app-settings-descriptor.js +262 -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/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 +97 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js +184 -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 +68 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +168 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
- package/dist/src/lib/config-object-descriptor.d.ts +108 -0
- package/dist/src/lib/config-object-descriptor.js +607 -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 +105 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +93 -0
- package/dist/src/lib/config-surface.js +178 -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 +53 -0
- package/dist/src/lib/config.js +92 -53
- 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 +89 -0
- package/dist/src/lib/credentials-store.js +330 -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/env-resolver.d.ts +62 -0
- package/dist/src/lib/env-resolver.js +121 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/fetch.d.ts +5 -0
- package/dist/src/lib/generated-allowlist.d.ts +28 -0
- package/dist/src/lib/generated-allowlist.js +266 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +578 -0
- package/dist/src/lib/generated-config-surfaces.js +3630 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-template-lint.d.ts +206 -0
- package/dist/src/lib/generated-template-lint.js +589 -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-config.d.ts +81 -0
- package/dist/src/lib/init-config.js +149 -0
- package/dist/src/lib/init-config.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +568 -0
- package/dist/src/lib/log-inspection.js +639 -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 +163 -0
- package/dist/src/lib/migration-nag.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 +51 -0
- package/dist/src/lib/platform-owned.js +64 -0
- package/dist/src/lib/platform-owned.js.map +1 -0
- package/dist/src/lib/project-config.d.ts +97 -0
- package/dist/src/lib/project-config.js +219 -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 +71 -0
- package/dist/src/lib/resolve-init-server.js +94 -0
- package/dist/src/lib/resolve-init-server.js.map +1 -0
- package/dist/src/lib/resolve-platform.d.ts +45 -0
- package/dist/src/lib/resolve-platform.js +43 -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/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 +99 -0
- package/dist/src/lib/snapshots.js +357 -0
- package/dist/src/lib/snapshots.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +100 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +682 -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 +85 -0
- package/dist/src/lib/swift-codegen/generator.js +266 -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 +90 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js +152 -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-paths.d.ts +72 -0
- package/dist/src/lib/sync-paths.js +130 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/sync-resource-types.d.ts +445 -0
- package/dist/src/lib/sync-resource-types.js +812 -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 +121 -0
- package/dist/src/lib/template.js +447 -67
- package/dist/src/lib/template.js.map +1 -1
- 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 +527 -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/workflow-apply.d.ts +51 -0
- package/dist/src/lib/workflow-apply.js +89 -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 +83 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +71 -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-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 +71 -0
- package/dist/src/lib/workflow-include-preserve.js +153 -0
- package/dist/src/lib/workflow-include-preserve.js.map +1 -0
- package/dist/src/lib/workflow-payload.d.ts +86 -0
- package/dist/src/lib/workflow-payload.js +168 -0
- package/dist/src/lib/workflow-payload.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.d.ts +199 -0
- package/dist/src/lib/workflow-toml-validator.js +754 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- package/dist/src/types/index.d.ts +533 -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 +32 -6
|
@@ -1,21 +1,240 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, statSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, statSync, readdirSync, } from "fs";
|
|
2
2
|
import { basename } from "path";
|
|
3
|
-
import * as
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { stringifyConfigToml } from "../lib/config-toml.js";
|
|
4
5
|
import { lookup as mimeLookup } from "mime-types";
|
|
6
|
+
import { generateWorkflowTypes, } from "../lib/workflow-codegen/generator.js";
|
|
7
|
+
import { generateWorkflowSwiftTypes } from "../lib/swift-codegen/generator.js";
|
|
5
8
|
import { ApiClient } from "../lib/api-client.js";
|
|
6
9
|
import { resolveAppId } from "../lib/config.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 { 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
14
|
import chalk from "chalk";
|
|
8
|
-
import { success, error, info, warn, keyValue, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, } from "../lib/output.js";
|
|
15
|
+
import { success, error, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, formatStatus, formatDuration, json, jsonLine, divider, dim, progress, progressEnd, } from "../lib/output.js";
|
|
16
|
+
import { pollUntil, followLoop, watchLoop, parseIntervalSeconds, } from "../lib/watch.js";
|
|
17
|
+
import { APPLY_RUN_STATUSES, RUN_STATUS_LIST, SETTLED_RUN_STATUSES, } from "../lib/run-status.js";
|
|
18
|
+
/**
|
|
19
|
+
* Render the `Step Results` section of `workflows runs status`. Pure
|
|
20
|
+
* formatting helper — returns lines as strings so it's unit-testable
|
|
21
|
+
* (the caller console.logs them). For each step, emits:
|
|
22
|
+
*
|
|
23
|
+
* ` <id>: <status-label> <duration>`
|
|
24
|
+
*
|
|
25
|
+
* and, when applicable, indented follow-up lines:
|
|
26
|
+
*
|
|
27
|
+
* - `error_captured` step with `error` → ` error: <truncated message>`
|
|
28
|
+
* - forEach output with `errors[]` non-empty →
|
|
29
|
+
* ` forEach errors: <N/M> - first: <truncated message>`
|
|
30
|
+
*
|
|
31
|
+
* The truncation length matches `runs steps` ERROR column (80 chars,
|
|
32
|
+
* trailing "..." when truncated). See #688.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Stop-polling predicate for the `workflows preview --wait` loop (issues #941,
|
|
36
|
+
* #2348). True when the run has settled and the wait should stop — as opposed
|
|
37
|
+
* to a still-in-flight status ("queued"/"running") that keeps the loop going
|
|
38
|
+
* until the cap.
|
|
39
|
+
*
|
|
40
|
+
* Since #2348 the server reconciles every status surface onto the canonical
|
|
41
|
+
* vocabulary before it goes on the wire, so this reads the one shared set in
|
|
42
|
+
* `lib/run-status.ts` instead of re-deriving terminality here:
|
|
43
|
+
* - "completed" / "failed" / "terminated" — true terminals. "terminated" is
|
|
44
|
+
* an aborted preview, a real end state (Codex P2 on #941: it used to be
|
|
45
|
+
* omitted, so an abort was misreported as a wait timeout).
|
|
46
|
+
* - "apply_pending" / "apply_claimed" — the run finished and is parked
|
|
47
|
+
* waiting for a client to apply its result. It never advances on its own,
|
|
48
|
+
* so the previous ladder spun here until the 60s cap.
|
|
49
|
+
* - "missing" — no live instance to observe; more polling cannot help.
|
|
50
|
+
*
|
|
51
|
+
* The raw Cloudflare spellings ("complete"/"errored"/"waiting") are gone: the
|
|
52
|
+
* server no longer puts them on the wire, and matching them here would have
|
|
53
|
+
* kept two vocabularies alive.
|
|
54
|
+
*/
|
|
55
|
+
export function isPreviewWaitTerminalStatus(s) {
|
|
56
|
+
return typeof s === "string" && SETTLED_RUN_STATUSES.has(s);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Which arm the `workflows preview --wait` render ladder takes (#2636).
|
|
60
|
+
*
|
|
61
|
+
* Extracted from the ladder so the branch decision is testable: the point of
|
|
62
|
+
* the `skipped` arm is that an elided preview — one whose declarative lock was
|
|
63
|
+
* held under `onContention: "ignore"` — reports that it did not run, instead
|
|
64
|
+
* of falling through to the timeout branch and exiting 124 as though it were
|
|
65
|
+
* still going.
|
|
66
|
+
*/
|
|
67
|
+
export function previewWaitOutcome(status) {
|
|
68
|
+
if (status === "completed")
|
|
69
|
+
return "completed";
|
|
70
|
+
if (status === "failed")
|
|
71
|
+
return "failed";
|
|
72
|
+
if (status === "terminated")
|
|
73
|
+
return "terminated";
|
|
74
|
+
if (status === "skipped")
|
|
75
|
+
return "skipped";
|
|
76
|
+
if (typeof status === "string" && APPLY_RUN_STATUSES.has(status))
|
|
77
|
+
return "apply";
|
|
78
|
+
if (status === "missing")
|
|
79
|
+
return "missing";
|
|
80
|
+
return "timeout";
|
|
81
|
+
}
|
|
82
|
+
export function renderRunStatusStepResults(stepResults) {
|
|
83
|
+
const lines = [];
|
|
84
|
+
for (const step of stepResults || []) {
|
|
85
|
+
const statusLabel = step.skipped
|
|
86
|
+
? chalk.gray("skipped")
|
|
87
|
+
: step.status === "error_captured"
|
|
88
|
+
? chalk.yellow("error_captured")
|
|
89
|
+
: step.status === "failed"
|
|
90
|
+
? chalk.red("failed")
|
|
91
|
+
: step.status === "completed"
|
|
92
|
+
? chalk.green("completed")
|
|
93
|
+
: step.status || "?";
|
|
94
|
+
const durationLabel = formatDuration(step.durationMs);
|
|
95
|
+
lines.push(` ${step.id}: ${statusLabel} ${durationLabel}`);
|
|
96
|
+
// Single-step error_captured: show the captured error message.
|
|
97
|
+
if (step.status === "error_captured" && step.error) {
|
|
98
|
+
const errMsg = typeof step.error === "string" ? step.error : String(step.error);
|
|
99
|
+
const truncated = errMsg.length > 80 ? errMsg.slice(0, 77) + "..." : errMsg;
|
|
100
|
+
lines.push(` ${chalk.dim("error:")} ${chalk.yellow(truncated)}`);
|
|
101
|
+
}
|
|
102
|
+
else if (step.status === "error_captured" && step.errorDetails) {
|
|
103
|
+
// No top-level error but errorDetails present — hint to drill in.
|
|
104
|
+
lines.push(` ${chalk.dim("error: <see runs step-detail for full errorDetails>")}`);
|
|
105
|
+
}
|
|
106
|
+
// forEach aggregated errors: a one-line summary surfaces the count
|
|
107
|
+
// of `error_captured` iterations and a sample message (#688).
|
|
108
|
+
const fo = step.output;
|
|
109
|
+
if (fo && typeof fo === "object" && Array.isArray(fo.errors) && fo.errors.length > 0) {
|
|
110
|
+
const total = (Array.isArray(fo.items) ? fo.items.length : 0)
|
|
111
|
+
|| (fo.totalSucceeded ?? 0) + (fo.totalFailed ?? 0);
|
|
112
|
+
const firstErr = (Array.isArray(fo.errorMessages) && fo.errorMessages.length > 0
|
|
113
|
+
? fo.errorMessages[0]
|
|
114
|
+
: (fo.errors[0]?.error ?? "?"));
|
|
115
|
+
const firstStr = typeof firstErr === "string" ? firstErr : String(firstErr);
|
|
116
|
+
const truncated = firstStr.length > 80 ? firstStr.slice(0, 77) + "..." : firstStr;
|
|
117
|
+
lines.push(` ${chalk.dim("forEach errors:")} ${chalk.yellow(`${fo.totalFailed ?? fo.errors.length}/${total}`)} ${chalk.dim("- first:")} ${chalk.yellow(truncated)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return lines;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* #1367 — annotate step-run rows with the inter-step gap: the wall-clock time
|
|
124
|
+
* between the previous step's `endedAt` and this step's `startedAt`. Sorts by
|
|
125
|
+
* `startedAt` when every row has a parseable one (tie-break `stepIndex`),
|
|
126
|
+
* otherwise by `stepIndex` for the whole list — the ordering mode must be
|
|
127
|
+
* chosen once for all rows, not per pair: a per-pair timestamp-vs-index
|
|
128
|
+
* fallback is not transitive when some rows lack a timestamp (A<B by index,
|
|
129
|
+
* B<C by index, C<A by time), and a non-transitive comparator leaves
|
|
130
|
+
* Array.prototype.sort free to return an arbitrary order, computing gaps
|
|
131
|
+
* against the wrong predecessor. The first step has no predecessor, so its
|
|
132
|
+
* `gapMs` is `null`. A gap is only meaningful when both adjacent timestamps
|
|
133
|
+
* parse and the gap is positive. When the immediate predecessor lacks a valid
|
|
134
|
+
* `endedAt`, the gap is left `null` rather than measured from an older,
|
|
135
|
+
* non-adjacent step — otherwise a later step could raise a false stall
|
|
136
|
+
* warning against a step it does not actually follow. Exported for
|
|
137
|
+
* testability.
|
|
138
|
+
*/
|
|
139
|
+
export function annotateStepGaps(steps) {
|
|
140
|
+
const rows = [...(steps || [])];
|
|
141
|
+
const allTimed = rows.every((r) => r?.startedAt && !Number.isNaN(new Date(r.startedAt).getTime()));
|
|
142
|
+
rows.sort((a, b) => {
|
|
143
|
+
if (allTimed) {
|
|
144
|
+
const sa = new Date(a.startedAt).getTime();
|
|
145
|
+
const sb = new Date(b.startedAt).getTime();
|
|
146
|
+
if (sa !== sb)
|
|
147
|
+
return sa - sb;
|
|
148
|
+
}
|
|
149
|
+
return (a?.stepIndex ?? 0) - (b?.stepIndex ?? 0);
|
|
150
|
+
});
|
|
151
|
+
let prevEnded = null;
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
const started = row?.startedAt ? new Date(row.startedAt).getTime() : NaN;
|
|
154
|
+
if (prevEnded != null && !Number.isNaN(started)) {
|
|
155
|
+
const gap = started - prevEnded;
|
|
156
|
+
row.gapMs = gap > 0 ? gap : 0;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
row.gapMs = null;
|
|
160
|
+
}
|
|
161
|
+
// Only genuinely adjacent steps yield a gap: if this step lacks a valid
|
|
162
|
+
// `endedAt`, clear the predecessor so the NEXT step's gap is left null
|
|
163
|
+
// instead of being measured from a non-adjacent earlier step.
|
|
164
|
+
const ended = row?.endedAt ? new Date(row.endedAt).getTime() : NaN;
|
|
165
|
+
prevEnded = Number.isNaN(ended) ? null : ended;
|
|
166
|
+
}
|
|
167
|
+
return rows;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* #1367 — compute min / p50 / p95 / max over a set of latency samples (ms).
|
|
171
|
+
* Ignores non-finite values. Returns `null` when there are no valid samples.
|
|
172
|
+
* Percentiles use nearest-rank on the sorted samples. Exported for testability.
|
|
173
|
+
*/
|
|
174
|
+
export function summarizeLatencyDistribution(values) {
|
|
175
|
+
const nums = (values || []).filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
176
|
+
if (nums.length === 0)
|
|
177
|
+
return null;
|
|
178
|
+
nums.sort((a, b) => a - b);
|
|
179
|
+
const pct = (p) => {
|
|
180
|
+
const rank = Math.ceil((p / 100) * nums.length);
|
|
181
|
+
return nums[Math.min(nums.length - 1, Math.max(0, rank - 1))];
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
count: nums.length,
|
|
185
|
+
min: nums[0],
|
|
186
|
+
p50: pct(50),
|
|
187
|
+
p95: pct(95),
|
|
188
|
+
max: nums[nums.length - 1],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Issue #687 (review feedback): render an unambiguous label for a workflow
|
|
193
|
+
* config slot ("active config", "draft config", etc.) when the slot may be
|
|
194
|
+
* unnamed. Falls back to a short ID prefix so users staging multiple unnamed
|
|
195
|
+
* configs can still tell them apart.
|
|
196
|
+
*
|
|
197
|
+
* formatConfigSlotLabel("active config", "v2", "01KRT...") → 'active config "v2"'
|
|
198
|
+
* formatConfigSlotLabel("active config", null, "01KRTABCDXYZ...") → 'active config (01KRTABCDXYZ…)'
|
|
199
|
+
* formatConfigSlotLabel("active config", null, null) → 'active config'
|
|
200
|
+
*
|
|
201
|
+
* Exported for testability.
|
|
202
|
+
*/
|
|
203
|
+
export function formatConfigSlotLabel(slot, configName, configId, prefixLength = 12) {
|
|
204
|
+
if (configName) {
|
|
205
|
+
return `${slot} "${configName}"`;
|
|
206
|
+
}
|
|
207
|
+
if (configId) {
|
|
208
|
+
return `${slot} (${String(configId).slice(0, prefixLength)}…)`;
|
|
209
|
+
}
|
|
210
|
+
return slot;
|
|
211
|
+
}
|
|
9
212
|
export function registerWorkflowsCommands(program) {
|
|
10
213
|
const workflows = program
|
|
11
214
|
.command("workflows")
|
|
12
|
-
.description("
|
|
215
|
+
.description("Inspect workflows, run and monitor them, and take one out of service")
|
|
13
216
|
.addHelpText("after", `
|
|
14
217
|
Examples:
|
|
15
218
|
$ primitive workflows list
|
|
16
|
-
$ primitive
|
|
17
|
-
$ primitive
|
|
219
|
+
$ primitive config new workflow process-doc # scaffold workflows/process-doc.toml
|
|
220
|
+
$ primitive sync push --only workflow/process-doc # apply it
|
|
18
221
|
$ primitive workflows runs list 01HXY...
|
|
222
|
+
$ primitive workflows disable 01HXY... # take it out of service now
|
|
223
|
+
`);
|
|
224
|
+
// Server-side configuration is authored in TOML and applied with `sync push`
|
|
225
|
+
// (issue #2645) — this group no longer carries config-setting flags.
|
|
226
|
+
workflows.addHelpText("after", `
|
|
227
|
+
Configuration lives in workflows/<key>.toml:
|
|
228
|
+
$ primitive config fields workflow # the file's keys, types and defaults
|
|
229
|
+
$ primitive config new workflow <key> # scaffold one locally
|
|
230
|
+
$ primitive config set workflow/<key> <path>=<value>
|
|
231
|
+
$ primitive sync push --only workflow/<key>
|
|
232
|
+
|
|
233
|
+
Which body runs is configuration too: name it with activeConfigName in
|
|
234
|
+
workflows/<key>.toml (bodies live in workflows/<key>.configs/<name>.toml) and
|
|
235
|
+
apply it with 'sync push'. The one exception is 'workflows publish', which
|
|
236
|
+
promotes the legacy draft slot on a workflow with NO active config — see its
|
|
237
|
+
--help.
|
|
19
238
|
`);
|
|
20
239
|
// List workflows
|
|
21
240
|
workflows
|
|
@@ -53,78 +272,10 @@ Examples:
|
|
|
53
272
|
process.exit(1);
|
|
54
273
|
}
|
|
55
274
|
});
|
|
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("--json", "Output as JSON")
|
|
67
|
-
.action(async (appId, options) => {
|
|
68
|
-
const resolvedAppId = resolveAppId(appId, options);
|
|
69
|
-
const client = new ApiClient();
|
|
70
|
-
let payload;
|
|
71
|
-
if (options.fromFile) {
|
|
72
|
-
try {
|
|
73
|
-
const content = readFileSync(options.fromFile, "utf-8");
|
|
74
|
-
const tomlData = TOML.parse(content);
|
|
75
|
-
const workflow = tomlData.workflow || tomlData;
|
|
76
|
-
payload = {
|
|
77
|
-
workflowKey: workflow.key || workflow.workflowKey,
|
|
78
|
-
name: workflow.name,
|
|
79
|
-
description: workflow.description,
|
|
80
|
-
steps: tomlData.steps || [],
|
|
81
|
-
inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
|
|
82
|
-
outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
|
|
83
|
-
perUserMaxRunning: workflow.perUserMaxRunning,
|
|
84
|
-
perUserMaxQueued: workflow.perUserMaxQueued,
|
|
85
|
-
perAppMaxRunning: workflow.perAppMaxRunning,
|
|
86
|
-
perAppMaxQueued: workflow.perAppMaxQueued,
|
|
87
|
-
queueTtlSeconds: workflow.queueTtlSeconds,
|
|
88
|
-
dequeueOrder: workflow.dequeueOrder,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
error(`Failed to read TOML file: ${err.message}`);
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
if (!options.key || !options.name) {
|
|
98
|
-
error("Required: --key, --name (or use --from-file)");
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
payload = {
|
|
102
|
-
workflowKey: options.key,
|
|
103
|
-
name: options.name,
|
|
104
|
-
description: options.description,
|
|
105
|
-
steps: [],
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
const result = await client.createWorkflow(resolvedAppId, payload);
|
|
110
|
-
if (options.json) {
|
|
111
|
-
json(result);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
success(`Workflow created: ${result.workflow?.name || options.name}`);
|
|
115
|
-
keyValue("Workflow ID", result.workflow?.workflowId);
|
|
116
|
-
keyValue("Key", result.workflow?.workflowKey);
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
error(err.message);
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
// Get workflow
|
|
124
275
|
workflows
|
|
125
276
|
.command("get")
|
|
126
277
|
.description("Get workflow details")
|
|
127
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
278
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
128
279
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
129
280
|
.option("--json", "Output as JSON")
|
|
130
281
|
.action(async (workflowId, options) => {
|
|
@@ -137,19 +288,36 @@ Examples:
|
|
|
137
288
|
return;
|
|
138
289
|
}
|
|
139
290
|
const wf = result.workflow;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
291
|
+
printResult("Workflow ID", wf.workflowId);
|
|
292
|
+
printResult("Key", wf.workflowKey);
|
|
293
|
+
printResult("Name", wf.name);
|
|
294
|
+
printResult("Description", wf.description);
|
|
295
|
+
printResult("Status", formatStatus(wf.status));
|
|
296
|
+
// #2645 criterion 9 — `status` is what the repo configured; this is
|
|
297
|
+
// whether an operator has pulled it out of service right now. Two
|
|
298
|
+
// different questions, so `get` answers both.
|
|
299
|
+
printResult("In Service", wf.operationallyDisabled === true
|
|
300
|
+
? "no (operationally disabled — `workflows enable` to restore)"
|
|
301
|
+
: "yes");
|
|
302
|
+
printResult("Active Config", wf.activeConfigId || "-");
|
|
303
|
+
printResult("Latest Revision", wf.latestRevision || "-");
|
|
304
|
+
printResult("Client Apply", wf.requiresClientApply !== false ? "yes" : "no");
|
|
305
|
+
printResult("Sync Callable", wf.syncCallable === true ? "yes" : "no");
|
|
306
|
+
// #1233 — runAs unset means the engine defaults to "caller"; show that
|
|
307
|
+
// default rather than a blank so the deployed principal mode is visible.
|
|
308
|
+
printResult("Run As", wf.runAs || "caller");
|
|
309
|
+
printResult("Capabilities", Array.isArray(wf.capabilities) && wf.capabilities.length > 0
|
|
310
|
+
? wf.capabilities.join(", ")
|
|
311
|
+
: "-");
|
|
312
|
+
if (wf.accessRule) {
|
|
313
|
+
printResult("Access Rule", wf.accessRule);
|
|
314
|
+
}
|
|
147
315
|
divider();
|
|
148
316
|
info("Queue Settings:");
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
317
|
+
printResult(" Per User Max Running", wf.perUserMaxRunning);
|
|
318
|
+
printResult(" Per User Max Queued", wf.perUserMaxQueued);
|
|
319
|
+
printResult(" Per App Max Running", wf.perAppMaxRunning);
|
|
320
|
+
printResult(" Dequeue Order", wf.dequeueOrder);
|
|
153
321
|
if (wf.inputSchema) {
|
|
154
322
|
divider();
|
|
155
323
|
info("Input Schema:");
|
|
@@ -190,147 +358,112 @@ Examples:
|
|
|
190
358
|
process.exit(1);
|
|
191
359
|
}
|
|
192
360
|
});
|
|
193
|
-
// Update workflow
|
|
194
361
|
workflows
|
|
195
|
-
.command("
|
|
196
|
-
.description("
|
|
197
|
-
.argument("<
|
|
198
|
-
.option("--
|
|
199
|
-
.
|
|
200
|
-
.option("--description <desc>", "Description")
|
|
201
|
-
.option("--status <status>", "Status: draft, active, archived")
|
|
202
|
-
.option("--per-user-max-running <n>", "Max running per user")
|
|
203
|
-
.option("--per-user-max-queued <n>", "Max queued per user")
|
|
204
|
-
.option("--dequeue-order <order>", "Dequeue order: fifo, lifo")
|
|
205
|
-
.option("--json", "Output as JSON")
|
|
206
|
-
.action(async (workflowId, options) => {
|
|
207
|
-
const resolvedAppId = resolveAppId(undefined, options);
|
|
208
|
-
const payload = {};
|
|
209
|
-
if (options.name)
|
|
210
|
-
payload.name = options.name;
|
|
211
|
-
if (options.description)
|
|
212
|
-
payload.description = options.description;
|
|
213
|
-
if (options.status)
|
|
214
|
-
payload.status = options.status;
|
|
215
|
-
if (options.perUserMaxRunning)
|
|
216
|
-
payload.perUserMaxRunning = parseInt(options.perUserMaxRunning);
|
|
217
|
-
if (options.perUserMaxQueued)
|
|
218
|
-
payload.perUserMaxQueued = parseInt(options.perUserMaxQueued);
|
|
219
|
-
if (options.dequeueOrder)
|
|
220
|
-
payload.dequeueOrder = options.dequeueOrder;
|
|
221
|
-
if (Object.keys(payload).length === 0) {
|
|
222
|
-
error("No update options specified.");
|
|
223
|
-
process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
const client = new ApiClient();
|
|
362
|
+
.command("expand")
|
|
363
|
+
.description("Expand a workflow TOML's include fragments and print the result")
|
|
364
|
+
.argument("<file>", "Path to the workflow TOML file")
|
|
365
|
+
.option("--format <format>", "Output format: json (default) or toml", "json")
|
|
366
|
+
.action((file, options) => {
|
|
226
367
|
try {
|
|
227
|
-
const
|
|
228
|
-
if (options.
|
|
229
|
-
|
|
230
|
-
|
|
368
|
+
const expanded = expandWorkflow(file);
|
|
369
|
+
if (options.format === "toml") {
|
|
370
|
+
// Strip the `include` key (already removed by expander) and emit
|
|
371
|
+
// valid TOML so the result is round-trippable.
|
|
372
|
+
console.log(stringifyConfigToml(expanded));
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
console.log(JSON.stringify(expanded, null, 2));
|
|
231
376
|
}
|
|
232
|
-
success("Workflow updated.");
|
|
233
377
|
}
|
|
234
378
|
catch (err) {
|
|
235
|
-
error(err
|
|
379
|
+
error(err?.message ?? String(err));
|
|
236
380
|
process.exit(1);
|
|
237
381
|
}
|
|
238
382
|
});
|
|
239
|
-
//
|
|
383
|
+
// #2645 criterion 9 — the operational toggle.
|
|
384
|
+
//
|
|
385
|
+
// Configuration is authored in TOML and applied with `sync push`, which would
|
|
386
|
+
// otherwise mean an operator needs a repo checkout to stop a misbehaving
|
|
387
|
+
// workflow. It does not: taking something out of service is an OPERATION, and
|
|
388
|
+
// these two verbs are its only writers. They change no configuration field —
|
|
389
|
+
// `status` still says whatever the committed TOML says — so the repo keeps
|
|
390
|
+
// describing the server and `sync diff` reports the object as "operationally
|
|
391
|
+
// disabled; configuration unchanged" rather than as drift.
|
|
240
392
|
workflows
|
|
241
|
-
.command("
|
|
242
|
-
.description("
|
|
243
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
393
|
+
.command("disable")
|
|
394
|
+
.description("Take a workflow out of service now. Operational, not configuration: no TOML field changes, and 'sync push' cannot set or clear it.")
|
|
395
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
244
396
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
245
|
-
.option("--
|
|
246
|
-
.option("-y, --yes", "Skip confirmation prompt")
|
|
397
|
+
.option("--json", "Output as JSON")
|
|
247
398
|
.action(async (workflowId, options) => {
|
|
248
399
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
249
|
-
if (!options.yes) {
|
|
250
|
-
const action = options.hard ? "permanently delete" : "archive";
|
|
251
|
-
const inquirer = await import("inquirer");
|
|
252
|
-
const { confirm } = await inquirer.default.prompt([
|
|
253
|
-
{
|
|
254
|
-
type: "confirm",
|
|
255
|
-
name: "confirm",
|
|
256
|
-
message: `Are you sure you want to ${action} workflow ${workflowId}?`,
|
|
257
|
-
default: false,
|
|
258
|
-
},
|
|
259
|
-
]);
|
|
260
|
-
if (!confirm) {
|
|
261
|
-
info("Cancelled.");
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
400
|
const client = new ApiClient();
|
|
266
401
|
try {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
else {
|
|
272
|
-
await client.updateWorkflow(resolvedAppId, workflowId, { status: "archived" });
|
|
273
|
-
success("Workflow archived.");
|
|
402
|
+
const result = await client.disableWorkflow(resolvedAppId, workflowId);
|
|
403
|
+
if (options.json) {
|
|
404
|
+
json(result);
|
|
405
|
+
return;
|
|
274
406
|
}
|
|
407
|
+
success(`Workflow disabled: ${result.workflowKey || workflowId}`);
|
|
408
|
+
info("New runs are refused. Configuration is unchanged — `primitive workflows enable` puts it back.");
|
|
275
409
|
}
|
|
276
410
|
catch (err) {
|
|
277
411
|
error(err.message);
|
|
278
412
|
process.exit(1);
|
|
279
413
|
}
|
|
280
414
|
});
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
.command("update")
|
|
286
|
-
.description("Update workflow draft steps")
|
|
287
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
415
|
+
workflows
|
|
416
|
+
.command("enable")
|
|
417
|
+
.description("Put an operationally disabled workflow back in service.")
|
|
418
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
288
419
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
289
|
-
.option("--from-file <path>", "Load steps from TOML file")
|
|
290
420
|
.option("--json", "Output as JSON")
|
|
291
421
|
.action(async (workflowId, options) => {
|
|
292
422
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
293
|
-
if (!options.fromFile) {
|
|
294
|
-
error("--from-file is required");
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
let payload;
|
|
298
|
-
try {
|
|
299
|
-
const content = readFileSync(options.fromFile, "utf-8");
|
|
300
|
-
const tomlData = TOML.parse(content);
|
|
301
|
-
const workflow = tomlData.workflow || tomlData;
|
|
302
|
-
payload = {
|
|
303
|
-
steps: tomlData.steps || [],
|
|
304
|
-
inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
|
|
305
|
-
outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
catch (err) {
|
|
309
|
-
error(`Failed to read TOML file: ${err.message}`);
|
|
310
|
-
process.exit(1);
|
|
311
|
-
}
|
|
312
423
|
const client = new ApiClient();
|
|
313
424
|
try {
|
|
314
|
-
const result = await client.
|
|
425
|
+
const result = await client.enableWorkflow(resolvedAppId, workflowId);
|
|
315
426
|
if (options.json) {
|
|
316
427
|
json(result);
|
|
317
428
|
return;
|
|
318
429
|
}
|
|
319
|
-
success(
|
|
320
|
-
|
|
430
|
+
success(`Workflow enabled: ${result.workflowKey || workflowId}`);
|
|
431
|
+
// `status` is configuration, so enabling does not publish a draft.
|
|
432
|
+
if (result.status && result.status !== "active") {
|
|
433
|
+
warn(`Status is "${result.status}", so it still will not run. Status is configuration — set it in workflows/<key>.toml and 'sync push'.`);
|
|
434
|
+
}
|
|
321
435
|
}
|
|
322
436
|
catch (err) {
|
|
323
437
|
error(err.message);
|
|
324
438
|
process.exit(1);
|
|
325
439
|
}
|
|
326
440
|
});
|
|
327
|
-
//
|
|
441
|
+
// The one write verb that survives the TOML-only rule (#2645), and only for
|
|
442
|
+
// the workflows the rule cannot reach: a definition with NO active config —
|
|
443
|
+
// i.e. one created before the config model, since creation has minted a
|
|
444
|
+
// config ever since. On those, `sync push` writes the legacy DRAFT slot
|
|
445
|
+
// (`workflow-apply.ts`) while execution reads the active revision, so
|
|
446
|
+
// promoting the pushed body is a step nothing else performs. For every other
|
|
447
|
+
// workflow the server refuses this call and `activeConfigName` is the answer.
|
|
328
448
|
workflows
|
|
329
449
|
.command("publish")
|
|
330
|
-
.description("
|
|
331
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
450
|
+
.description("Promote the legacy draft slot to a new revision. Legacy only: works on a workflow with no active config, and the server refuses it otherwise.")
|
|
451
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
332
452
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
333
453
|
.option("--json", "Output as JSON")
|
|
454
|
+
.addHelpText("after", `
|
|
455
|
+
The exception to TOML-only authoring, and a narrow one.
|
|
456
|
+
|
|
457
|
+
This publishes the LEGACY draft slot, which only a workflow with no active
|
|
458
|
+
config still has — workflows created before the configuration model. On those,
|
|
459
|
+
'sync push' writes the draft and the running revision is unchanged until this
|
|
460
|
+
promotes it, so removing this command would strand them.
|
|
461
|
+
|
|
462
|
+
Every other workflow is authored in its file: the body lives in
|
|
463
|
+
workflows/<key>.configs/<name>.toml, activeConfigName in workflows/<key>.toml
|
|
464
|
+
names the one that runs, and 'sync push' applies both. The server rejects this
|
|
465
|
+
command for such a workflow rather than bumping a pointer that changes nothing.
|
|
466
|
+
`)
|
|
334
467
|
.action(async (workflowId, options) => {
|
|
335
468
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
336
469
|
const client = new ApiClient();
|
|
@@ -343,8 +476,27 @@ Examples:
|
|
|
343
476
|
success("Workflow published.");
|
|
344
477
|
keyValue("Revision ID", result.revision?.revisionId);
|
|
345
478
|
keyValue("Published At", formatDate(result.revision?.publishedAt));
|
|
479
|
+
// Issue #687: surface server-side deprecation hint when applicable.
|
|
480
|
+
if (result.deprecation) {
|
|
481
|
+
warn(`[deprecated] ${result.deprecation}`);
|
|
482
|
+
}
|
|
346
483
|
}
|
|
347
484
|
catch (err) {
|
|
485
|
+
// Issue #987: 'publish' is inert on config-model workflows (production
|
|
486
|
+
// runs read the active config), so the server rejects with a 409.
|
|
487
|
+
// Point the user at the config primitives instead of a raw API error.
|
|
488
|
+
if (err?.statusCode === 409 && err?.code === "WORKFLOW_CONFIG_MODEL") {
|
|
489
|
+
error("This workflow has an active configuration, so 'publish' is inert " +
|
|
490
|
+
"(it bumps a legacy pointer that doesn't change what runs). For dev-loop edits, use\n" +
|
|
491
|
+
"'sync push' — it writes the active config directly and activates the workflow end-to-end:\n" +
|
|
492
|
+
" primitive sync push\n" +
|
|
493
|
+
"For staged rollouts, author the config as a sidecar and name it in the workflow file:\n" +
|
|
494
|
+
" workflows/<key>.configs/<name>.toml # the config body\n" +
|
|
495
|
+
" activeConfigName = \"<name>\" # in workflows/<key>.toml\n" +
|
|
496
|
+
" primitive config set workflow/<key> workflow.status=active\n" +
|
|
497
|
+
" primitive sync push --only workflow/<key>");
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
348
500
|
error(err.message);
|
|
349
501
|
process.exit(1);
|
|
350
502
|
}
|
|
@@ -353,14 +505,20 @@ Examples:
|
|
|
353
505
|
workflows
|
|
354
506
|
.command("preview")
|
|
355
507
|
.description("Run a preview execution of the workflow")
|
|
356
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
508
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
357
509
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
358
|
-
.option("--config <config-id>", "Use specific configuration
|
|
510
|
+
.option("--config <config-id>", "Use specific configuration")
|
|
511
|
+
.option("--draft", "Force preview of the draft version, even if active is newer")
|
|
512
|
+
.option("--active", "Force preview of the active config, even if a newer draft exists (issue #687)")
|
|
359
513
|
.option("--input <json>", "Root input as JSON")
|
|
360
514
|
.option("--wait", "Wait for completion and show result")
|
|
361
515
|
.option("--json", "Output as JSON")
|
|
362
516
|
.action(async (workflowId, options) => {
|
|
363
517
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
518
|
+
if (options.draft && options.active) {
|
|
519
|
+
error("--draft and --active are mutually exclusive");
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
364
522
|
let rootInput;
|
|
365
523
|
if (options.input) {
|
|
366
524
|
try {
|
|
@@ -376,7 +534,15 @@ Examples:
|
|
|
376
534
|
const result = await client.previewWorkflow(resolvedAppId, workflowId, {
|
|
377
535
|
rootInput,
|
|
378
536
|
configId: options.config,
|
|
537
|
+
useDraft: options.draft || false,
|
|
538
|
+
// Issue #687: --active forces the active config even when a newer
|
|
539
|
+
// draft exists (the inverse of --draft).
|
|
540
|
+
preferActive: options.active || false,
|
|
379
541
|
});
|
|
542
|
+
// Display warning from the server (e.g. "previewing draft because newer than active")
|
|
543
|
+
if (result.warning) {
|
|
544
|
+
warn(result.warning);
|
|
545
|
+
}
|
|
380
546
|
if (!options.wait) {
|
|
381
547
|
if (options.json) {
|
|
382
548
|
json(result);
|
|
@@ -384,26 +550,77 @@ Examples:
|
|
|
384
550
|
}
|
|
385
551
|
success("Preview started.");
|
|
386
552
|
keyValue("Instance ID", result.instanceId);
|
|
553
|
+
// Issue #687: name the side we ran so the user always knows which
|
|
554
|
+
// version produced the output (the "print the source" pattern).
|
|
555
|
+
const source = result.source;
|
|
556
|
+
if (source === "draft") {
|
|
557
|
+
info("Previewing draft version.");
|
|
558
|
+
}
|
|
559
|
+
else if (source === "active") {
|
|
560
|
+
// Issue #687 (review feedback): when configName is missing,
|
|
561
|
+
// fall back to a short ID prefix so users staging multiple
|
|
562
|
+
// unnamed configs can disambiguate.
|
|
563
|
+
const label = formatConfigSlotLabel("active config", result.configName, result.configId);
|
|
564
|
+
info(`Previewing ${label}.`);
|
|
565
|
+
}
|
|
566
|
+
else if (source === "config") {
|
|
567
|
+
const label = formatConfigSlotLabel("the requested config", result.configName, result.configId);
|
|
568
|
+
info(`Previewing ${label}.`);
|
|
569
|
+
}
|
|
387
570
|
info("Use 'workflows runs status' to check progress.");
|
|
388
571
|
return;
|
|
389
572
|
}
|
|
390
|
-
// Poll for completion
|
|
573
|
+
// Poll for completion. Issue #941: track whether the loop broke on a
|
|
574
|
+
// terminal state vs. ran to exhaustion, so a timed-out wait is
|
|
575
|
+
// distinguishable from a workflow that legitimately reported "running".
|
|
391
576
|
info("Waiting for completion...");
|
|
392
577
|
let status;
|
|
393
|
-
|
|
578
|
+
// Fixed ~60s cap (60 × 1s sleep + RTT). PRIMITIVE_PREVIEW_WAIT_ATTEMPTS
|
|
579
|
+
// overrides the iteration count for tests that exercise the
|
|
580
|
+
// poll-loop-exhaustion path without a full 60s wait; unset in normal use.
|
|
581
|
+
const maxAttempts = (() => {
|
|
582
|
+
const raw = process.env.PRIMITIVE_PREVIEW_WAIT_ATTEMPTS;
|
|
583
|
+
const parsed = raw ? parseInt(raw, 10) : NaN;
|
|
584
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 60;
|
|
585
|
+
})();
|
|
586
|
+
let reachedTerminal = false;
|
|
394
587
|
for (let i = 0; i < maxAttempts; i++) {
|
|
395
588
|
await new Promise((r) => setTimeout(r, 1000));
|
|
396
589
|
const statusResult = await client.getPreviewStatus(resolvedAppId, workflowId, result.instanceId);
|
|
397
590
|
status = statusResult.status;
|
|
398
|
-
|
|
591
|
+
const s = status?.status;
|
|
592
|
+
// Issue #941 (Codex P2): an aborted preview reports
|
|
593
|
+
// status:"terminated" from the admin preview-status endpoint, which
|
|
594
|
+
// is a real terminal state — the wait did NOT time out. The shared
|
|
595
|
+
// predicate includes it so the loop breaks and `waitTimedOut` stays
|
|
596
|
+
// false (vs. the prior check that omitted "terminated" and let the
|
|
597
|
+
// loop run to the cap, misreporting an abort as a timeout).
|
|
598
|
+
if (isPreviewWaitTerminalStatus(s)) {
|
|
599
|
+
reachedTerminal = true;
|
|
399
600
|
break;
|
|
400
601
|
}
|
|
401
602
|
}
|
|
603
|
+
// Issue #941: on poll-loop exhaustion with a still-non-terminal run,
|
|
604
|
+
// emit an explicit timeout signal and exit non-zero (124, the GNU
|
|
605
|
+
// `timeout` convention) instead of silently returning the bare
|
|
606
|
+
// non-terminal status as if the wait succeeded.
|
|
607
|
+
const waitTimedOut = !reachedTerminal;
|
|
402
608
|
if (options.json) {
|
|
403
|
-
|
|
609
|
+
// Additive field only — `status` is preserved so existing parsers
|
|
610
|
+
// don't break; `waitTimedOut` lets automation tell "still running,
|
|
611
|
+
// wait gave up" from "finished".
|
|
612
|
+
json({ instanceId: result.instanceId, status, waitTimedOut });
|
|
613
|
+
if (waitTimedOut) {
|
|
614
|
+
process.exit(124);
|
|
615
|
+
}
|
|
404
616
|
return;
|
|
405
617
|
}
|
|
406
|
-
|
|
618
|
+
// #2348: this render ladder used to re-derive terminality (and matched
|
|
619
|
+
// the raw "complete"/"errored" spellings the server no longer emits).
|
|
620
|
+
// It now branches on the same canonical values the poll loop stops on,
|
|
621
|
+
// via the shared sets in lib/run-status.ts.
|
|
622
|
+
const waitOutcome = previewWaitOutcome(status?.status);
|
|
623
|
+
if (waitOutcome === "completed") {
|
|
407
624
|
success("Preview completed.");
|
|
408
625
|
if (status.output) {
|
|
409
626
|
console.log("\nOutput:");
|
|
@@ -417,15 +634,45 @@ Examples:
|
|
|
417
634
|
});
|
|
418
635
|
}
|
|
419
636
|
}
|
|
420
|
-
else if (
|
|
637
|
+
else if (waitOutcome === "failed") {
|
|
421
638
|
error("Preview failed.");
|
|
422
639
|
if (status.error) {
|
|
423
640
|
console.log("\nError:");
|
|
424
641
|
console.log(JSON.stringify(status.error, null, 2));
|
|
425
642
|
}
|
|
426
643
|
}
|
|
644
|
+
else if (waitOutcome === "terminated") {
|
|
645
|
+
// Issue #941 (Codex P2): the preview was aborted (e.g. via
|
|
646
|
+
// `workflows preview ... abort`, or an external terminate). This is a
|
|
647
|
+
// real terminal state — NOT a timeout — so render it distinctly and
|
|
648
|
+
// exit 0 (the wait succeeded in observing a terminal result).
|
|
649
|
+
warn("Preview was terminated (aborted before completion).");
|
|
650
|
+
}
|
|
651
|
+
else if (waitOutcome === "skipped") {
|
|
652
|
+
// #2636 — the preview's declarative lock was held and the definition
|
|
653
|
+
// declared `onContention: "ignore"`, so it never ran. That is an
|
|
654
|
+
// outcome, not a timeout: report it and exit 0.
|
|
655
|
+
warn(`Preview did not run — lock held${status?.skipReason ? ` (${status.skipReason})` : ""}.`);
|
|
656
|
+
}
|
|
657
|
+
else if (waitOutcome === "apply") {
|
|
658
|
+
// #2348: the run finished and is parked waiting for a client to claim
|
|
659
|
+
// and apply its result. It will not advance on its own, so the wait
|
|
660
|
+
// stopping here is a success, not a timeout.
|
|
661
|
+
warn(`Preview finished and is waiting to be applied (status: ${status.status}).`);
|
|
662
|
+
if (status.output) {
|
|
663
|
+
console.log("\nOutput:");
|
|
664
|
+
console.log(JSON.stringify(status.output, null, 2));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else if (waitOutcome === "missing") {
|
|
668
|
+
// #2348: no live instance to observe — reporting a timeout here would
|
|
669
|
+
// suggest the preview is still running, which it is not.
|
|
670
|
+
error("Preview instance not found (status: missing).");
|
|
671
|
+
}
|
|
427
672
|
else {
|
|
428
|
-
|
|
673
|
+
// Timed out without ever reaching a terminal state (issue #941).
|
|
674
|
+
error(`Wait timed out after ${maxAttempts}s; preview still running. Check status with 'workflows runs status'.`);
|
|
675
|
+
process.exit(124);
|
|
429
676
|
}
|
|
430
677
|
}
|
|
431
678
|
catch (err) {
|
|
@@ -433,39 +680,513 @@ Examples:
|
|
|
433
680
|
process.exit(1);
|
|
434
681
|
}
|
|
435
682
|
});
|
|
683
|
+
// #1367 — hidden queue-latency measurement tool. Fires a batch of runs of a
|
|
684
|
+
// (trivial, no-op) workflow, polls until each has an executionStartedAt, and
|
|
685
|
+
// reports the observed queueDelayMs / createCallDurationMs distribution. This
|
|
686
|
+
// is the post-deploy verification vehicle: local miniflare does not exhibit
|
|
687
|
+
// real Cloudflare scheduling delay, so run it against an agents/alpha scratch
|
|
688
|
+
// app. Creates are paced (default 10/s) to stay clear of CF's ~100/s shared
|
|
689
|
+
// instance-creation limit; a create failure (e.g. 429) is recorded, not fatal.
|
|
690
|
+
workflows
|
|
691
|
+
.command("queue-latency-test", { hidden: true })
|
|
692
|
+
.description("(hidden) Fire N no-op runs and report the queue-delay distribution (post-deploy tool)")
|
|
693
|
+
.argument("<workflow-id>", "Workflow ID or key of a trivial no-op workflow")
|
|
694
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
695
|
+
.option("--count <n>", "Number of runs to fire", "50")
|
|
696
|
+
.option("--rate <r>", "Max creates per second (<=10 recommended)", "10")
|
|
697
|
+
.option("--input <json>", "Root input as JSON")
|
|
698
|
+
.option("--timeout <sec>", "Seconds to wait for all runs to start", "120")
|
|
699
|
+
.option("--verbose", "Print a per-run row")
|
|
700
|
+
.option("--json", "Output as JSON")
|
|
701
|
+
.action(async (workflowId, options) => {
|
|
702
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
703
|
+
const client = new ApiClient();
|
|
704
|
+
const count = Math.max(1, parseInt(options.count, 10) || 50);
|
|
705
|
+
const rate = Math.max(1, parseInt(options.rate, 10) || 10);
|
|
706
|
+
const timeoutSec = Math.max(1, parseInt(options.timeout, 10) || 120);
|
|
707
|
+
const intervalMs = Math.ceil(1000 / rate);
|
|
708
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
709
|
+
let rootInput;
|
|
710
|
+
if (options.input) {
|
|
711
|
+
try {
|
|
712
|
+
rootInput = JSON.parse(options.input);
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
error("Invalid JSON in --input");
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const fired = [];
|
|
720
|
+
if (!options.json) {
|
|
721
|
+
info(`Firing ${count} run(s) of ${workflowId} at up to ${rate}/s (interval ${intervalMs}ms)...`);
|
|
722
|
+
}
|
|
723
|
+
for (let i = 0; i < count; i++) {
|
|
724
|
+
try {
|
|
725
|
+
const res = await client.previewWorkflow(resolvedAppId, workflowId, {
|
|
726
|
+
rootInput,
|
|
727
|
+
});
|
|
728
|
+
fired.push({
|
|
729
|
+
index: i,
|
|
730
|
+
instanceId: res?.instanceId,
|
|
731
|
+
createFailed: false,
|
|
732
|
+
started: false,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
// A create failure (e.g. HTTP 500 from CF's shared-class rate limit) is
|
|
737
|
+
// a recorded datapoint, not a reason to abort the whole batch.
|
|
738
|
+
fired.push({
|
|
739
|
+
index: i,
|
|
740
|
+
createFailed: true,
|
|
741
|
+
createError: err?.message ? String(err.message) : String(err),
|
|
742
|
+
started: false,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
if (i < count - 1)
|
|
746
|
+
await sleep(intervalMs);
|
|
747
|
+
}
|
|
748
|
+
// Poll the run list until every non-failed run has executionStartedAt set
|
|
749
|
+
// (queueDelayMs present) or we hit the timeout.
|
|
750
|
+
const byInstance = new Map();
|
|
751
|
+
for (const f of fired) {
|
|
752
|
+
if (f.instanceId)
|
|
753
|
+
byInstance.set(f.instanceId, f);
|
|
754
|
+
}
|
|
755
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
756
|
+
const pending = () => fired.filter((f) => !f.createFailed && !f.started).length;
|
|
757
|
+
// The admin run-list endpoint caps each page at 200, so a single large
|
|
758
|
+
// request cannot cover a batch of more than 200 runs — the earliest ones
|
|
759
|
+
// would never be matched and would be falsely reported as not started.
|
|
760
|
+
// Page through with the cursor (runs come back newest-first, so our batch
|
|
761
|
+
// is at the front) until every pending run is matched or we run out of
|
|
762
|
+
// pages. Bound the scan so we never walk the whole run history.
|
|
763
|
+
const PAGE_LIMIT = 200;
|
|
764
|
+
const scanBudget = count + PAGE_LIMIT;
|
|
765
|
+
while (pending() > 0 && Date.now() < deadline) {
|
|
766
|
+
await sleep(1000);
|
|
767
|
+
try {
|
|
768
|
+
let cursor;
|
|
769
|
+
let scanned = 0;
|
|
770
|
+
do {
|
|
771
|
+
const { items, cursor: nextCursor } = await client.listWorkflowRuns(resolvedAppId, workflowId, { limit: PAGE_LIMIT, cursor: cursor ?? undefined });
|
|
772
|
+
for (const run of items || []) {
|
|
773
|
+
const f = run?.instanceId ? byInstance.get(run.instanceId) : undefined;
|
|
774
|
+
if (f && !f.started && run.executionStartedAt) {
|
|
775
|
+
f.started = true;
|
|
776
|
+
f.queueDelayMs =
|
|
777
|
+
typeof run.queueDelayMs === "number" ? run.queueDelayMs : null;
|
|
778
|
+
f.createCallDurationMs =
|
|
779
|
+
typeof run.createCallDurationMs === "number"
|
|
780
|
+
? run.createCallDurationMs
|
|
781
|
+
: null;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
scanned += items?.length || 0;
|
|
785
|
+
cursor = nextCursor;
|
|
786
|
+
} while (pending() > 0 && cursor && scanned < scanBudget);
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
// Transient list error — keep polling until the deadline.
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const createFailures = fired.filter((f) => f.createFailed).length;
|
|
793
|
+
const startedRuns = fired.filter((f) => f.started);
|
|
794
|
+
const queueDelays = startedRuns.map((f) => f.queueDelayMs);
|
|
795
|
+
const createDurations = startedRuns.map((f) => f.createCallDurationMs);
|
|
796
|
+
const queueDist = summarizeLatencyDistribution(queueDelays);
|
|
797
|
+
const createDist = summarizeLatencyDistribution(createDurations);
|
|
798
|
+
const notStarted = fired.filter((f) => !f.createFailed && !f.started).length;
|
|
799
|
+
if (options.json) {
|
|
800
|
+
json({
|
|
801
|
+
workflowId,
|
|
802
|
+
requested: count,
|
|
803
|
+
created: count - createFailures,
|
|
804
|
+
createFailures,
|
|
805
|
+
started: startedRuns.length,
|
|
806
|
+
notStartedWithinTimeout: notStarted,
|
|
807
|
+
queueDelayMs: queueDist,
|
|
808
|
+
createCallDurationMs: createDist,
|
|
809
|
+
runs: options.verbose
|
|
810
|
+
? fired.map((f) => ({
|
|
811
|
+
index: f.index,
|
|
812
|
+
instanceId: f.instanceId ?? null,
|
|
813
|
+
createFailed: f.createFailed,
|
|
814
|
+
createError: f.createError ?? null,
|
|
815
|
+
started: f.started,
|
|
816
|
+
queueDelayMs: f.queueDelayMs ?? null,
|
|
817
|
+
createCallDurationMs: f.createCallDurationMs ?? null,
|
|
818
|
+
}))
|
|
819
|
+
: undefined,
|
|
820
|
+
});
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
divider();
|
|
824
|
+
keyValue("Requested", count);
|
|
825
|
+
keyValue("Created", count - createFailures);
|
|
826
|
+
keyValue("Create failures", createFailures);
|
|
827
|
+
keyValue("Started", startedRuns.length);
|
|
828
|
+
keyValue("Not started (timeout)", notStarted);
|
|
829
|
+
divider();
|
|
830
|
+
if (queueDist) {
|
|
831
|
+
info("Queue delay (executionStartedAt - requested):");
|
|
832
|
+
keyValue(" min", formatDuration(queueDist.min));
|
|
833
|
+
keyValue(" p50", formatDuration(queueDist.p50));
|
|
834
|
+
keyValue(" p95", formatDuration(queueDist.p95));
|
|
835
|
+
keyValue(" max", formatDuration(queueDist.max));
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
warn("No runs started — no queue-delay samples collected.");
|
|
839
|
+
}
|
|
840
|
+
if (createDist) {
|
|
841
|
+
divider();
|
|
842
|
+
info("Create-call duration:");
|
|
843
|
+
keyValue(" min", formatDuration(createDist.min));
|
|
844
|
+
keyValue(" p50", formatDuration(createDist.p50));
|
|
845
|
+
keyValue(" p95", formatDuration(createDist.p95));
|
|
846
|
+
keyValue(" max", formatDuration(createDist.max));
|
|
847
|
+
}
|
|
848
|
+
if (options.verbose) {
|
|
849
|
+
divider();
|
|
850
|
+
console.log(formatTable(fired.map((f) => ({
|
|
851
|
+
run: f.index,
|
|
852
|
+
instanceId: f.instanceId ?? "-",
|
|
853
|
+
status: f.createFailed ? "create-failed" : f.started ? "started" : "pending",
|
|
854
|
+
queueDelayMs: f.queueDelayMs,
|
|
855
|
+
createCallDurationMs: f.createCallDurationMs,
|
|
856
|
+
})), [
|
|
857
|
+
{ header: "RUN", key: "run" },
|
|
858
|
+
{ header: "INSTANCE", key: "instanceId", format: formatId },
|
|
859
|
+
{ header: "STATE", key: "status" },
|
|
860
|
+
{ header: "DELAY", key: "queueDelayMs", format: formatDuration },
|
|
861
|
+
{ header: "CREATE", key: "createCallDurationMs", format: formatDuration },
|
|
862
|
+
]));
|
|
863
|
+
}
|
|
864
|
+
});
|
|
436
865
|
// Runs subcommand
|
|
437
866
|
const runs = workflows.command("runs").description("Manage workflow runs");
|
|
438
867
|
// List runs
|
|
868
|
+
const runsTableColumns = [
|
|
869
|
+
{ header: "RUN ID", key: "runId", format: formatId },
|
|
870
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
871
|
+
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
872
|
+
// #1367 — queue delay (requested → execution start).
|
|
873
|
+
{ header: "DELAY", key: "queueDelayMs", format: formatDuration },
|
|
874
|
+
{ header: "ENDED", key: "endedAt", format: formatDate },
|
|
875
|
+
{ header: "PREVIEW", key: "isPreview", format: (v) => (v ? "yes" : "") },
|
|
876
|
+
];
|
|
439
877
|
runs
|
|
440
878
|
.command("list")
|
|
441
879
|
.description("List workflow runs")
|
|
442
|
-
|
|
880
|
+
// #1967 — the workflow id is optional when `--user-id` is given: that
|
|
881
|
+
// form lists one user's runs across every workflow.
|
|
882
|
+
.argument("[workflow-id]", "Workflow ID or key (optional with --user-id)")
|
|
443
883
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
444
|
-
|
|
884
|
+
// #2348: the canonical run-status vocabulary. The old text advertised
|
|
885
|
+
// "pending", which no server path ever writes, and omitted "terminated".
|
|
886
|
+
// An unrecognised value is now a 400 from the server, not an empty page.
|
|
887
|
+
.option("--status <status>", `Filter by status: ${RUN_STATUS_LIST}`)
|
|
888
|
+
.option("--user-id <user-id>", "List runs started by this user, across every workflow")
|
|
445
889
|
.option("--limit <n>", "Number of runs to show", "20")
|
|
890
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous page")
|
|
891
|
+
// #1969 — shared tailing flags. `--watch` re-renders the current list each
|
|
892
|
+
// interval (snapshot); `--follow` appends new/changed runs (tail). Mutually
|
|
893
|
+
// exclusive; `--interval` floor is 1s (default 2s).
|
|
894
|
+
.option("--watch", "Re-render the current run list every interval (snapshot)")
|
|
895
|
+
.option("--follow", "Append runs as they start or change (tail)")
|
|
896
|
+
.option("--interval <seconds>", "Poll interval in seconds (min 1, default 2)")
|
|
446
897
|
.option("--json", "Output as JSON")
|
|
447
898
|
.action(async (workflowId, options) => {
|
|
448
899
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
449
900
|
const client = new ApiClient();
|
|
901
|
+
const limit = parseInt(options.limit);
|
|
902
|
+
if (options.watch && options.follow) {
|
|
903
|
+
error("--watch and --follow are mutually exclusive; pass only one.");
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
// #1967 — one of the two selectors is required; `list` never enumerates
|
|
907
|
+
// every run in the app.
|
|
908
|
+
if (!workflowId && !options.userId) {
|
|
909
|
+
error("Provide a workflow ID, or --user-id to list one user's runs across every workflow.");
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
// The user view reads the runs-by-user index, which has no resume
|
|
913
|
+
// contract — `--follow`'s checkpoint is keyed on the by-workflow index.
|
|
914
|
+
if (options.userId && options.follow) {
|
|
915
|
+
error("--follow is not supported with --user-id (it reads a different index). Use --watch instead.");
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
// Both selectors resolve through the same page shape, so the rest of the
|
|
919
|
+
// command is identical either way.
|
|
920
|
+
const fetchPage = (params) => options.userId
|
|
921
|
+
? client.listUserWorkflowRuns(resolvedAppId, options.userId, {
|
|
922
|
+
...params,
|
|
923
|
+
// With both a workflow id and --user-id, narrow to that workflow.
|
|
924
|
+
workflowId: workflowId || undefined,
|
|
925
|
+
})
|
|
926
|
+
: client.listWorkflowRuns(resolvedAppId, workflowId, params);
|
|
927
|
+
// One-shot list (no tailing).
|
|
928
|
+
if (!options.watch && !options.follow) {
|
|
929
|
+
try {
|
|
930
|
+
const page = await fetchPage({
|
|
931
|
+
status: options.status,
|
|
932
|
+
limit,
|
|
933
|
+
cursor: options.cursor,
|
|
934
|
+
});
|
|
935
|
+
const { items } = page;
|
|
936
|
+
if (options.json) {
|
|
937
|
+
// #1316 envelope with the shared inspection item shape — never a
|
|
938
|
+
// bare array — preserving nextCursor/hasMore + the deprecated
|
|
939
|
+
// `cursor` dual-emit alias (behaviors 6, 7).
|
|
940
|
+
json(buildWorkflowRunEnvelope(items, page));
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
// The cursor hint prints whether or not this page had rows: a
|
|
944
|
+
// filtered read can come back empty with more of the index still to
|
|
945
|
+
// walk, and a bare "No runs found." would read as "this user has no
|
|
946
|
+
// matching runs" (#1967 review).
|
|
947
|
+
const printCursorHint = () => {
|
|
948
|
+
if (!page.nextCursor)
|
|
949
|
+
return;
|
|
950
|
+
console.log();
|
|
951
|
+
dim(`More runs. Use --cursor ${page.nextCursor} for the next page.`);
|
|
952
|
+
};
|
|
953
|
+
if (!items || items.length === 0) {
|
|
954
|
+
// #2237 — the honest empty state: a bounded filtered read can stop
|
|
955
|
+
// before it reaches a match, so the message says how far it got and
|
|
956
|
+
// how to continue instead of asserting there are none. `runs list`
|
|
957
|
+
// deliberately does NOT auto-resume like `runs failures` — it is a
|
|
958
|
+
// pager, and its `--limit` is a page size.
|
|
959
|
+
info(describeEmptyRunPage(page, !!options.cursor));
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
console.log(formatTable(items, runsTableColumns));
|
|
963
|
+
printCursorHint();
|
|
964
|
+
}
|
|
965
|
+
catch (err) {
|
|
966
|
+
error(err.message);
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
// Tailing (`--watch` / `--follow`). SIGINT → AbortController installed
|
|
972
|
+
// here at the command level; the handler is removed in `finally` so it
|
|
973
|
+
// never leaks past this command (unlike the persistent process.on).
|
|
974
|
+
let intervalSeconds;
|
|
450
975
|
try {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
976
|
+
intervalSeconds = parseIntervalSeconds(options.interval);
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
error(err.message);
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
const intervalMs = intervalSeconds * 1000;
|
|
983
|
+
const controller = new AbortController();
|
|
984
|
+
const onSigint = () => controller.abort();
|
|
985
|
+
process.on("SIGINT", onSigint);
|
|
986
|
+
try {
|
|
987
|
+
if (options.follow) {
|
|
988
|
+
const source = {
|
|
989
|
+
fetchAfter: async (resumeAfter) => {
|
|
990
|
+
if (resumeAfter === undefined) {
|
|
991
|
+
// Bootstrap: newest position, descending, one row. followLoop
|
|
992
|
+
// discards the items and keeps only the baseline checkpoint.
|
|
993
|
+
const page = await client.listWorkflowRuns(resolvedAppId, workflowId, {
|
|
994
|
+
follow: true,
|
|
995
|
+
forward: false,
|
|
996
|
+
limit: 1,
|
|
997
|
+
status: options.status,
|
|
998
|
+
});
|
|
999
|
+
return { items: page.items, hasMore: false, resumeAfter: page.resumeAfter };
|
|
1000
|
+
}
|
|
1001
|
+
const page = await client.listWorkflowRuns(resolvedAppId, workflowId, {
|
|
1002
|
+
follow: true,
|
|
1003
|
+
forward: true,
|
|
1004
|
+
after: resumeAfter,
|
|
1005
|
+
limit,
|
|
1006
|
+
status: options.status,
|
|
1007
|
+
});
|
|
1008
|
+
return {
|
|
1009
|
+
items: page.items,
|
|
1010
|
+
hasMore: !!page.hasMore,
|
|
1011
|
+
resumeAfter: page.resumeAfter,
|
|
1012
|
+
};
|
|
1013
|
+
},
|
|
1014
|
+
};
|
|
1015
|
+
if (!options.json) {
|
|
1016
|
+
info("Following runs (Ctrl-C to stop)…");
|
|
1017
|
+
}
|
|
1018
|
+
await followLoop(source, {
|
|
1019
|
+
intervalMs,
|
|
1020
|
+
signal: controller.signal,
|
|
1021
|
+
render: (items) => {
|
|
1022
|
+
if (options.json) {
|
|
1023
|
+
// One JSON object per line (a tail has no closing bracket to
|
|
1024
|
+
// wait for), each in the shared inspection item shape — the
|
|
1025
|
+
// same items the one-shot envelope carries (behavior 7).
|
|
1026
|
+
for (const run of items)
|
|
1027
|
+
jsonLine(toWorkflowRunInspectionRow(run));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
for (const run of items) {
|
|
1031
|
+
console.log(`${formatDate(run.startedAt)} ${formatStatus(run.status)} ${formatId(run.runId)}`);
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
// --watch: periodic snapshot redraw. The most recent page's
|
|
1038
|
+
// pagination fields are kept so the `--json` redraw emits the same
|
|
1039
|
+
// envelope the one-shot path does, not a bare array (behavior 7).
|
|
1040
|
+
let lastPage = {};
|
|
1041
|
+
await watchLoop(async () => {
|
|
1042
|
+
const page = await fetchPage({
|
|
1043
|
+
status: options.status,
|
|
1044
|
+
limit,
|
|
1045
|
+
cursor: options.cursor,
|
|
1046
|
+
});
|
|
1047
|
+
lastPage = page;
|
|
1048
|
+
return page.items;
|
|
1049
|
+
}, {
|
|
1050
|
+
intervalMs,
|
|
1051
|
+
signal: controller.signal,
|
|
1052
|
+
render: (items) => {
|
|
1053
|
+
if (options.json) {
|
|
1054
|
+
json(buildWorkflowRunEnvelope(items, lastPage));
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
// Renderer owns the redraw: clear the screen, then print.
|
|
1058
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
1059
|
+
if (!items || items.length === 0) {
|
|
1060
|
+
console.log("No runs found.");
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
console.log(formatTable(items, runsTableColumns));
|
|
1064
|
+
},
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
finally {
|
|
1069
|
+
process.removeListener("SIGINT", onSigint);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
// Run status
|
|
1073
|
+
runs
|
|
1074
|
+
.command("status")
|
|
1075
|
+
.description("Get status of a workflow run")
|
|
1076
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1077
|
+
.argument("<run-id>", "Run ID")
|
|
1078
|
+
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1079
|
+
.option("--json", "Output as JSON")
|
|
1080
|
+
.action(async (workflowId, runId, options) => {
|
|
1081
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1082
|
+
const client = new ApiClient();
|
|
1083
|
+
try {
|
|
1084
|
+
const result = await client.getWorkflowRunStatus(resolvedAppId, workflowId, runId);
|
|
455
1085
|
if (options.json) {
|
|
456
|
-
json(
|
|
1086
|
+
json(result);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const run = result.run;
|
|
1090
|
+
keyValue("Run ID", run.runId);
|
|
1091
|
+
keyValue("Status", formatStatus(run.status));
|
|
1092
|
+
// #2636 — the machine-readable outcome next to the status: why a
|
|
1093
|
+
// failed run failed, or why an elided run never ran.
|
|
1094
|
+
if (run.errorCode)
|
|
1095
|
+
keyValue("Error code", run.errorCode);
|
|
1096
|
+
if (run.skipReason)
|
|
1097
|
+
keyValue("Skip reason", run.skipReason);
|
|
1098
|
+
// #1367 — `startedAt` is the request time; `executionStartedAt` is when
|
|
1099
|
+
// Cloudflare actually started running the instance. Queue delay is the
|
|
1100
|
+
// gap. A still-queued run shows "-" for execution start / queue delay.
|
|
1101
|
+
keyValue("Requested", formatDate(run.startedAt));
|
|
1102
|
+
keyValue("Execution started", formatDate(run.executionStartedAt));
|
|
1103
|
+
keyValue("Queue delay", formatDuration(run.queueDelayMs));
|
|
1104
|
+
keyValue("Create call", formatDuration(run.createCallDurationMs));
|
|
1105
|
+
keyValue("Ended", formatDate(run.endedAt));
|
|
1106
|
+
keyValue("Preview", run.isPreview ? "yes" : "no");
|
|
1107
|
+
if (result.instanceStatus) {
|
|
1108
|
+
const status = result.instanceStatus;
|
|
1109
|
+
if (status.output) {
|
|
1110
|
+
divider();
|
|
1111
|
+
info("Output:");
|
|
1112
|
+
console.log(JSON.stringify(status.output, null, 2));
|
|
1113
|
+
}
|
|
1114
|
+
if (status.stepResults && status.stepResults.length > 0) {
|
|
1115
|
+
divider();
|
|
1116
|
+
info("Step Results:");
|
|
1117
|
+
for (const line of renderRunStatusStepResults(status.stepResults)) {
|
|
1118
|
+
console.log(line);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (status.error) {
|
|
1122
|
+
divider();
|
|
1123
|
+
error("Error:");
|
|
1124
|
+
console.log(JSON.stringify(status.error, null, 2));
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
catch (err) {
|
|
1129
|
+
error(err.message);
|
|
1130
|
+
process.exit(1);
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
// Steps for a run
|
|
1134
|
+
runs
|
|
1135
|
+
.command("steps")
|
|
1136
|
+
.description("Show step-level details for a workflow run")
|
|
1137
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1138
|
+
.argument("<run-id>", "Run ID")
|
|
1139
|
+
.option("--app <app-id>", "App ID")
|
|
1140
|
+
.option("--json", "Output as JSON")
|
|
1141
|
+
.action(async (workflowId, runId, options) => {
|
|
1142
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1143
|
+
const client = new ApiClient();
|
|
1144
|
+
try {
|
|
1145
|
+
const { items } = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
|
|
1146
|
+
if (options.json) {
|
|
1147
|
+
// Steps are unpaginated (one run's full trace) — the shared
|
|
1148
|
+
// inspection item shape wrapped in `{items}`, no cursor (behavior 6).
|
|
1149
|
+
json(buildWorkflowStepEnvelope(items));
|
|
457
1150
|
return;
|
|
458
1151
|
}
|
|
459
1152
|
if (!items || items.length === 0) {
|
|
460
|
-
info("No runs found.");
|
|
1153
|
+
info("No step runs found.");
|
|
461
1154
|
return;
|
|
462
1155
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1156
|
+
// #1367 — inter-step gap: idle time between one step ending and the
|
|
1157
|
+
// next starting. Only surfaced above a 1s threshold so normal
|
|
1158
|
+
// back-to-back steps stay quiet; a large gap flags a mid-run stall.
|
|
1159
|
+
const STEP_GAP_THRESHOLD_MS = 1000;
|
|
1160
|
+
const rows = annotateStepGaps(items);
|
|
1161
|
+
console.log(formatTable(rows, [
|
|
1162
|
+
{ header: "STEP", key: "stepId" },
|
|
1163
|
+
{ header: "KIND", key: "stepKind" },
|
|
1164
|
+
{
|
|
1165
|
+
header: "STATUS",
|
|
1166
|
+
key: "status",
|
|
1167
|
+
format: (v) => v === "completed" ? chalk.green(v) :
|
|
1168
|
+
v === "failed" ? chalk.red(v) :
|
|
1169
|
+
v === "skipped" ? chalk.gray(v) :
|
|
1170
|
+
v === "error_captured" ? chalk.yellow(v) : v,
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
header: "GAP",
|
|
1174
|
+
key: "gapMs",
|
|
1175
|
+
format: (v) => typeof v === "number" && v >= STEP_GAP_THRESHOLD_MS
|
|
1176
|
+
? chalk.yellow(formatDuration(v))
|
|
1177
|
+
: chalk.dim("-"),
|
|
1178
|
+
},
|
|
1179
|
+
{ header: "DURATION", key: "durationMs", format: formatDuration },
|
|
1180
|
+
{
|
|
1181
|
+
header: "TOKENS",
|
|
1182
|
+
key: "totalTokens",
|
|
1183
|
+
format: (v) => v ? String(v).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : "-",
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
header: "ERROR",
|
|
1187
|
+
key: "error",
|
|
1188
|
+
format: (v) => v ? (v.length > 60 ? v.slice(0, 57) + "..." : v) : "",
|
|
1189
|
+
},
|
|
469
1190
|
]));
|
|
470
1191
|
}
|
|
471
1192
|
catch (err) {
|
|
@@ -473,102 +1194,363 @@ Examples:
|
|
|
473
1194
|
process.exit(1);
|
|
474
1195
|
}
|
|
475
1196
|
});
|
|
476
|
-
//
|
|
1197
|
+
// Step detail
|
|
477
1198
|
runs
|
|
478
|
-
.command("
|
|
479
|
-
.description("
|
|
480
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1199
|
+
.command("step-detail")
|
|
1200
|
+
.description("Show full details for a single step")
|
|
1201
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
481
1202
|
.argument("<run-id>", "Run ID")
|
|
1203
|
+
.argument("<step-id>", "Step ID")
|
|
1204
|
+
.option("--app <app-id>", "App ID")
|
|
1205
|
+
.option("--json", "Output as JSON")
|
|
1206
|
+
.action(async (workflowId, runId, stepId, options) => {
|
|
1207
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1208
|
+
const client = new ApiClient();
|
|
1209
|
+
try {
|
|
1210
|
+
const { items } = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
|
|
1211
|
+
const step = items.find((s) => s.stepId === stepId);
|
|
1212
|
+
if (!step) {
|
|
1213
|
+
error(`Step "${stepId}" not found in run ${runId}`);
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
}
|
|
1216
|
+
if (options.json) {
|
|
1217
|
+
json(step);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
keyValue("Step", step.stepId);
|
|
1221
|
+
keyValue("Kind", step.stepKind);
|
|
1222
|
+
keyValue("Status", formatStatus(step.status));
|
|
1223
|
+
keyValue("Duration", formatDuration(step.durationMs));
|
|
1224
|
+
keyValue("Started", formatDate(step.startedAt));
|
|
1225
|
+
keyValue("Ended", formatDate(step.endedAt));
|
|
1226
|
+
if (step.totalTokens) {
|
|
1227
|
+
keyValue("Tokens", `${step.inputTokens || 0} in / ${step.outputTokens || 0} out / ${step.totalTokens} total`);
|
|
1228
|
+
}
|
|
1229
|
+
if (step.retryCount) {
|
|
1230
|
+
keyValue("Retries", String(step.retryCount));
|
|
1231
|
+
}
|
|
1232
|
+
if (step.config) {
|
|
1233
|
+
divider();
|
|
1234
|
+
info("Config (rendered):");
|
|
1235
|
+
console.log(JSON.stringify(step.config, null, 2));
|
|
1236
|
+
}
|
|
1237
|
+
if (step.rawConfig) {
|
|
1238
|
+
divider();
|
|
1239
|
+
info("Config (raw):");
|
|
1240
|
+
console.log(JSON.stringify(step.rawConfig, null, 2));
|
|
1241
|
+
}
|
|
1242
|
+
if (step.input) {
|
|
1243
|
+
divider();
|
|
1244
|
+
info("Input:");
|
|
1245
|
+
console.log(JSON.stringify(step.input, null, 2));
|
|
1246
|
+
}
|
|
1247
|
+
if (step.output) {
|
|
1248
|
+
divider();
|
|
1249
|
+
info("Output:");
|
|
1250
|
+
console.log(JSON.stringify(step.output, null, 2));
|
|
1251
|
+
}
|
|
1252
|
+
if (step.error) {
|
|
1253
|
+
divider();
|
|
1254
|
+
error("Error: " + step.error);
|
|
1255
|
+
if (step.errorDetails) {
|
|
1256
|
+
console.log(step.errorDetails);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
if (step.templateWarnings && step.templateWarnings.length > 0) {
|
|
1260
|
+
divider();
|
|
1261
|
+
warn("Template Warnings:");
|
|
1262
|
+
step.templateWarnings.forEach((w) => {
|
|
1263
|
+
console.log(` ${w.expression}: ${w.message}`);
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
if (step.context) {
|
|
1267
|
+
divider();
|
|
1268
|
+
info("Context Snapshot:");
|
|
1269
|
+
console.log(JSON.stringify(step.context, null, 2));
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
catch (err) {
|
|
1273
|
+
error(err.message);
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
// Integration call logs for a run (cross-pivot from workflow-run to
|
|
1278
|
+
// integrations.logs — see issue #699).
|
|
1279
|
+
runs
|
|
1280
|
+
.command("logs")
|
|
1281
|
+
.description("List integration calls made by a specific workflow run (cross-pivot of `integrations logs`)")
|
|
1282
|
+
.argument("<run-id>", "Workflow run ID")
|
|
482
1283
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1284
|
+
.option("--limit <n>", "Number of logs to show", "100")
|
|
1285
|
+
.option("--json", "Output as JSON")
|
|
1286
|
+
.action(async (runId, options) => {
|
|
1287
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1288
|
+
const client = new ApiClient();
|
|
1289
|
+
try {
|
|
1290
|
+
const logs = await client.listWorkflowRunIntegrationLogs(resolvedAppId, runId, { limit: parseInt(options.limit) });
|
|
1291
|
+
if (options.json) {
|
|
1292
|
+
json(logs);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
if (!logs || logs.length === 0) {
|
|
1296
|
+
info("No integration calls found for this run.");
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
console.log(formatTable(logs, [
|
|
1300
|
+
{ header: "TIME", key: "timestamp", format: formatDate },
|
|
1301
|
+
{ header: "STEP", key: "stepId", format: (v) => v || "" },
|
|
1302
|
+
{
|
|
1303
|
+
header: "INTEGRATION",
|
|
1304
|
+
key: "integrationKey",
|
|
1305
|
+
format: (v) => v || "",
|
|
1306
|
+
},
|
|
1307
|
+
{ header: "METHOD", key: "method" },
|
|
1308
|
+
{ header: "PATH", key: "path" },
|
|
1309
|
+
{ header: "STATUS", key: "status" },
|
|
1310
|
+
{ header: "DURATION", key: "durationMs", format: formatDuration },
|
|
1311
|
+
{ header: "TRACE", key: "traceId", format: formatId },
|
|
1312
|
+
]));
|
|
1313
|
+
}
|
|
1314
|
+
catch (err) {
|
|
1315
|
+
error(err.message);
|
|
1316
|
+
process.exit(1);
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
// Error summary
|
|
1320
|
+
runs
|
|
1321
|
+
.command("error")
|
|
1322
|
+
.description("Show error details for a failed workflow run")
|
|
1323
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1324
|
+
.argument("<run-id>", "Run ID")
|
|
1325
|
+
.option("--app <app-id>", "App ID")
|
|
483
1326
|
.option("--json", "Output as JSON")
|
|
484
1327
|
.action(async (workflowId, runId, options) => {
|
|
485
1328
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
486
1329
|
const client = new ApiClient();
|
|
487
1330
|
try {
|
|
488
|
-
const
|
|
1331
|
+
const [runResult, stepsResult] = await Promise.all([
|
|
1332
|
+
client.getWorkflowRunStatus(resolvedAppId, workflowId, runId),
|
|
1333
|
+
client.getWorkflowStepRuns(resolvedAppId, workflowId, runId),
|
|
1334
|
+
]);
|
|
1335
|
+
// #2074 — one selection rule shared with the STEP column of
|
|
1336
|
+
// `runs failures`: prefer the run's own denormalized attribution, and
|
|
1337
|
+
// fall back to the lowest-`stepIndex` failed row (never "whatever the
|
|
1338
|
+
// query returned first") so the two views can't disagree.
|
|
1339
|
+
const failedStep = selectFailedStepRow(stepsResult.items, runResult.run?.failedStepId);
|
|
1340
|
+
const errorInfo = {
|
|
1341
|
+
runId,
|
|
1342
|
+
status: runResult.run?.status,
|
|
1343
|
+
// #2636 — the platform's classification of the failure, when it owns
|
|
1344
|
+
// one (a lost declarative-lock race, an exhausted lock wait).
|
|
1345
|
+
errorCode: runResult.run?.errorCode ?? null,
|
|
1346
|
+
errorMessage: runResult.run?.errorMessage || runResult.instanceStatus?.error?.message,
|
|
1347
|
+
failedStep: failedStep ? {
|
|
1348
|
+
stepId: failedStep.stepId,
|
|
1349
|
+
stepKind: failedStep.stepKind,
|
|
1350
|
+
error: failedStep.error,
|
|
1351
|
+
errorDetails: failedStep.errorDetails,
|
|
1352
|
+
input: failedStep.input,
|
|
1353
|
+
config: failedStep.config,
|
|
1354
|
+
} : null,
|
|
1355
|
+
};
|
|
1356
|
+
if (options.json) {
|
|
1357
|
+
json(errorInfo);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
keyValue("Run ID", runId);
|
|
1361
|
+
keyValue("Status", formatStatus(errorInfo.status));
|
|
1362
|
+
if (errorInfo.errorCode) {
|
|
1363
|
+
keyValue("Code", errorInfo.errorCode);
|
|
1364
|
+
}
|
|
1365
|
+
if (errorInfo.errorMessage) {
|
|
1366
|
+
divider();
|
|
1367
|
+
error("Error: " + errorInfo.errorMessage);
|
|
1368
|
+
}
|
|
1369
|
+
if (failedStep) {
|
|
1370
|
+
divider();
|
|
1371
|
+
info(`Failed Step: ${failedStep.stepId} (${failedStep.stepKind})`);
|
|
1372
|
+
if (failedStep.error) {
|
|
1373
|
+
error(" " + failedStep.error);
|
|
1374
|
+
}
|
|
1375
|
+
if (failedStep.errorDetails) {
|
|
1376
|
+
console.log(failedStep.errorDetails);
|
|
1377
|
+
}
|
|
1378
|
+
if (failedStep.input) {
|
|
1379
|
+
divider();
|
|
1380
|
+
info("Step Input:");
|
|
1381
|
+
console.log(JSON.stringify(failedStep.input, null, 2));
|
|
1382
|
+
}
|
|
1383
|
+
if (failedStep.config) {
|
|
1384
|
+
divider();
|
|
1385
|
+
info("Step Config:");
|
|
1386
|
+
console.log(JSON.stringify(failedStep.config, null, 2));
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
info("No failed step found in step runs.");
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
catch (err) {
|
|
1394
|
+
error(err.message);
|
|
1395
|
+
process.exit(1);
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
// Failures list
|
|
1399
|
+
runs
|
|
1400
|
+
.command("failures")
|
|
1401
|
+
.description("List recent workflow run failures")
|
|
1402
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
1403
|
+
.option("--app <app-id>", "App ID")
|
|
1404
|
+
// #2237 — `--limit` is the number of FAILURES to collect, as the help text
|
|
1405
|
+
// always claimed. It used to be passed straight through as the server's
|
|
1406
|
+
// bounded page limit, so a workflow with a long success streak answered
|
|
1407
|
+
// "No failures found." while failures sat just past the scan budget.
|
|
1408
|
+
.option("--limit <n>", "Number of failures to collect", "10")
|
|
1409
|
+
.option("--max-scan <n>", `Stop after examining this many runs, to the nearest request (each examines up to ${SERVER_SCAN_ROWS_PER_REQUEST}; 0 = no cap; a sweep issues at most ${FAILURE_SWEEP_MAX_REQUESTS} requests either way)`, String(DEFAULT_FAILURE_SWEEP_MAX_SCAN))
|
|
1410
|
+
// `--cursor` seeds the sweep's starting position; the sweep then resumes
|
|
1411
|
+
// itself, so it is only needed to continue an earlier capped search.
|
|
1412
|
+
.option("--cursor <cursor>", "Resume the search from a previous cursor")
|
|
1413
|
+
.option("--json", "Output as JSON")
|
|
1414
|
+
.action(async (workflowId, options) => {
|
|
1415
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1416
|
+
const client = new ApiClient();
|
|
1417
|
+
const limit = parseInt(options.limit, 10);
|
|
1418
|
+
const maxScan = parseInt(options.maxScan, 10);
|
|
1419
|
+
if (!Number.isFinite(limit) || limit < 1) {
|
|
1420
|
+
error("--limit must be a positive integer.");
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
if (!Number.isFinite(maxScan) || maxScan < 0) {
|
|
1424
|
+
error("--max-scan must be a non-negative integer (0 = no cap).");
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
// Ctrl-C during a multi-second sweep prints what was collected plus the
|
|
1428
|
+
// resume cursor rather than dropping the work on the floor. The handler
|
|
1429
|
+
// is removed in `finally` so it never leaks past this command.
|
|
1430
|
+
//
|
|
1431
|
+
// Registering a listener replaces Node's terminate-on-SIGINT default, and
|
|
1432
|
+
// `shouldStop` is only read between requests — so a first Ctrl-C during a
|
|
1433
|
+
// slow in-flight request would otherwise appear to do nothing and leave
|
|
1434
|
+
// the command unkillable. A second one exits immediately (#2237 review).
|
|
1435
|
+
let interrupted = false;
|
|
1436
|
+
let sigints = 0;
|
|
1437
|
+
const onSigint = () => {
|
|
1438
|
+
interrupted = true;
|
|
1439
|
+
if (++sigints > 1)
|
|
1440
|
+
process.exit(130);
|
|
1441
|
+
};
|
|
1442
|
+
process.on("SIGINT", onSigint);
|
|
1443
|
+
// Progress on stderr, TTY only, never in `--json` mode: a silent
|
|
1444
|
+
// multi-second command invites the Ctrl-C it is trying to survive.
|
|
1445
|
+
const showProgress = !options.json && !!process.stderr.isTTY;
|
|
1446
|
+
try {
|
|
1447
|
+
const sweep = await collectWorkflowFailures(({ cursor, limit: pageLimit }) => client.listWorkflowRuns(resolvedAppId, workflowId, {
|
|
1448
|
+
status: "failed",
|
|
1449
|
+
limit: pageLimit,
|
|
1450
|
+
cursor,
|
|
1451
|
+
}), {
|
|
1452
|
+
limit,
|
|
1453
|
+
maxScan,
|
|
1454
|
+
startCursor: options.cursor,
|
|
1455
|
+
shouldStop: () => interrupted,
|
|
1456
|
+
onProgress: showProgress
|
|
1457
|
+
? (scanned, collected) => progress(`Searching… ${scanned} runs examined, ${collected} failure${collected === 1 ? "" : "s"} found`)
|
|
1458
|
+
: undefined,
|
|
1459
|
+
});
|
|
1460
|
+
if (showProgress && sweep.requests >= 1) {
|
|
1461
|
+
// Clear the progress line so it does not sit above the table. One
|
|
1462
|
+
// request is the common case — `onProgress` fires after EVERY request
|
|
1463
|
+
// including the first, so guarding on `> 1` left the line on screen
|
|
1464
|
+
// with the cursor parked at its end (#2237 review).
|
|
1465
|
+
process.stderr.write("\r\x1b[K");
|
|
1466
|
+
}
|
|
1467
|
+
// A SIGINT that arrived after the sweep returned (while the table is
|
|
1468
|
+
// rendering) did not truncate anything, so only a sweep that actually
|
|
1469
|
+
// stopped on the interrupt reports 130. `exitCode` rather than
|
|
1470
|
+
// `process.exit()` so a piped stdout is not truncated mid-write.
|
|
1471
|
+
const exitInterrupted = sweep.stoppedBecause === "interrupted";
|
|
1472
|
+
const items = sweep.items;
|
|
489
1473
|
if (options.json) {
|
|
490
|
-
|
|
1474
|
+
// #2074 — the shared inspection item shape inside the endpoint's own
|
|
1475
|
+
// #1316 envelope, never a bare array. #2237 — `items` is the
|
|
1476
|
+
// concatenation across every resumed page, `scanned` their sum, and
|
|
1477
|
+
// `nextCursor` the last one observed.
|
|
1478
|
+
json(buildWorkflowRunEnvelope(items, {
|
|
1479
|
+
nextCursor: sweep.nextCursor,
|
|
1480
|
+
hasMore: !!sweep.nextCursor,
|
|
1481
|
+
...(sweep.scannedKnown ? { scanned: sweep.scanned } : {}),
|
|
1482
|
+
}));
|
|
1483
|
+
if (exitInterrupted)
|
|
1484
|
+
process.exitCode = 130;
|
|
491
1485
|
return;
|
|
492
1486
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
status.stepResults.forEach((step) => {
|
|
510
|
-
console.log(` ${step.id}: ${formatDuration(step.durationMs)} ${step.skipped ? "(skipped)" : ""}`);
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
if (status.error) {
|
|
514
|
-
divider();
|
|
515
|
-
error("Error:");
|
|
516
|
-
console.log(JSON.stringify(status.error, null, 2));
|
|
517
|
-
}
|
|
1487
|
+
if (items.length > 0) {
|
|
1488
|
+
console.log(formatTable(items, [
|
|
1489
|
+
{ header: "RUN ID", key: "runId", format: formatId },
|
|
1490
|
+
// STEP sits before ERROR so the truncated message stays the last
|
|
1491
|
+
// wide column and the step id is never the thing that gets cut.
|
|
1492
|
+
// `-` is a routine outcome, not a bug: launch-time failures
|
|
1493
|
+
// (create/schema/R2 aborts, reclaimed runs) legitimately have no
|
|
1494
|
+
// failed step, and runs that predate #2074 carry no attribution.
|
|
1495
|
+
{ header: "STEP", key: "failedStepId", format: formatFailureStepCell },
|
|
1496
|
+
// #2636 — the platform's classification, next to the message an
|
|
1497
|
+
// operator would otherwise have had to pattern-match.
|
|
1498
|
+
{ header: "CODE", key: "errorCode", format: formatFailureCodeCell },
|
|
1499
|
+
{ header: "ERROR", key: "errorMessage", format: formatFailureErrorCell },
|
|
1500
|
+
{ header: "STARTED", key: "startedAt", format: formatDate },
|
|
1501
|
+
{ header: "ENDED", key: "endedAt", format: formatDate },
|
|
1502
|
+
]));
|
|
518
1503
|
}
|
|
1504
|
+
// #2237 — replaces the unconditional "No failures found.": every form
|
|
1505
|
+
// either states the search was complete or says how far it got and how
|
|
1506
|
+
// to continue. A triage command must never assert an absence it did
|
|
1507
|
+
// not establish.
|
|
1508
|
+
const summary = describeFailureSweep(sweep, limit);
|
|
1509
|
+
if (summary) {
|
|
1510
|
+
if (items.length > 0)
|
|
1511
|
+
console.log();
|
|
1512
|
+
info(summary);
|
|
1513
|
+
}
|
|
1514
|
+
if (exitInterrupted)
|
|
1515
|
+
process.exitCode = 130;
|
|
519
1516
|
}
|
|
520
1517
|
catch (err) {
|
|
521
1518
|
error(err.message);
|
|
522
1519
|
process.exit(1);
|
|
523
1520
|
}
|
|
1521
|
+
finally {
|
|
1522
|
+
process.removeListener("SIGINT", onSigint);
|
|
1523
|
+
}
|
|
524
1524
|
});
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
.description("Manage workflow configurations")
|
|
531
|
-
.addHelpText("after", `
|
|
532
|
-
Examples:
|
|
533
|
-
$ primitive workflows configs list <workflow-id>
|
|
534
|
-
$ primitive workflows configs get <workflow-id> <config-id>
|
|
535
|
-
$ primitive workflows configs create <workflow-id> --name "v2" --from-file workflow.toml
|
|
536
|
-
$ primitive workflows configs activate <workflow-id> <config-id>
|
|
537
|
-
$ primitive workflows configs duplicate <workflow-id> <config-id> --name "v2-copy"
|
|
538
|
-
`);
|
|
539
|
-
// List configs
|
|
540
|
-
configs
|
|
1525
|
+
// ── Iterations: inspect / reset iterate-users singleton iterations (#1209) ──
|
|
1526
|
+
const iterations = workflows
|
|
1527
|
+
.command("iterations")
|
|
1528
|
+
.description("Inspect and reset iterate-users iterations");
|
|
1529
|
+
iterations
|
|
541
1530
|
.command("list")
|
|
542
|
-
.description("List
|
|
543
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1531
|
+
.description("List iterate-users iterations for the app")
|
|
544
1532
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
545
1533
|
.option("--json", "Output as JSON")
|
|
546
|
-
.action(async (
|
|
1534
|
+
.action(async (options) => {
|
|
547
1535
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
548
1536
|
const client = new ApiClient();
|
|
549
1537
|
try {
|
|
550
|
-
const { items } = await client.
|
|
1538
|
+
const { items } = await client.listIterations(resolvedAppId);
|
|
551
1539
|
if (options.json) {
|
|
552
1540
|
json(items);
|
|
553
1541
|
return;
|
|
554
1542
|
}
|
|
555
1543
|
if (!items || items.length === 0) {
|
|
556
|
-
info("No
|
|
1544
|
+
info("No iterations found.");
|
|
557
1545
|
return;
|
|
558
1546
|
}
|
|
559
|
-
// Get workflow to find active config
|
|
560
|
-
const { workflow } = await client.getWorkflow(resolvedAppId, workflowId);
|
|
561
|
-
const activeConfigId = workflow?.activeConfigId;
|
|
562
1547
|
console.log(formatTable(items, [
|
|
563
|
-
{ header: "
|
|
564
|
-
{ header: "NAME", key: "configName" },
|
|
1548
|
+
{ header: "ITERATION", key: "iterationName" },
|
|
565
1549
|
{ header: "STATUS", key: "status", format: formatStatus },
|
|
566
|
-
{
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
},
|
|
571
|
-
{ header: "MODIFIED", key: "modifiedAt", format: formatDate },
|
|
1550
|
+
{ header: "ACQUIRED", key: "lastAcquireMode", format: (v) => v || "" },
|
|
1551
|
+
{ header: "PROCESSED", key: "totalProcessed" },
|
|
1552
|
+
{ header: "FAILED", key: "failedCount" },
|
|
1553
|
+
{ header: "LAST RUN", key: "lastRunAt", format: formatDate },
|
|
572
1554
|
]));
|
|
573
1555
|
}
|
|
574
1556
|
catch (err) {
|
|
@@ -576,35 +1558,44 @@ Examples:
|
|
|
576
1558
|
process.exit(1);
|
|
577
1559
|
}
|
|
578
1560
|
});
|
|
579
|
-
|
|
580
|
-
configs
|
|
1561
|
+
iterations
|
|
581
1562
|
.command("get")
|
|
582
|
-
.description("
|
|
583
|
-
.argument("<
|
|
584
|
-
.argument("<config-id>", "Configuration ID")
|
|
1563
|
+
.description("Show details for a single iterate-users iteration")
|
|
1564
|
+
.argument("<name>", "Iteration name")
|
|
585
1565
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
586
1566
|
.option("--json", "Output as JSON")
|
|
587
|
-
.action(async (
|
|
1567
|
+
.action(async (name, options) => {
|
|
588
1568
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
589
1569
|
const client = new ApiClient();
|
|
590
1570
|
try {
|
|
591
|
-
const
|
|
1571
|
+
const it = await client.getIteration(resolvedAppId, name);
|
|
592
1572
|
if (options.json) {
|
|
593
|
-
json(
|
|
1573
|
+
json(it);
|
|
594
1574
|
return;
|
|
595
1575
|
}
|
|
596
|
-
keyValue("
|
|
597
|
-
keyValue("
|
|
598
|
-
keyValue("
|
|
599
|
-
keyValue("
|
|
600
|
-
keyValue("
|
|
601
|
-
keyValue("
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1576
|
+
keyValue("Iteration", it.iterationName);
|
|
1577
|
+
keyValue("Status", formatStatus(it.status));
|
|
1578
|
+
keyValue("Acquired as", it.lastAcquireMode || "(unknown)");
|
|
1579
|
+
keyValue("Run ID", it.runId);
|
|
1580
|
+
keyValue("Instance ID", it.instanceId);
|
|
1581
|
+
keyValue("Started", formatDate(it.startedAt));
|
|
1582
|
+
keyValue("Last run", formatDate(it.lastRunAt));
|
|
1583
|
+
keyValue("Lock expires", formatDate(it.lockExpiresAt));
|
|
1584
|
+
keyValue("Processed", String(it.totalProcessed ?? 0));
|
|
1585
|
+
keyValue("Succeeded", String(it.succeededCount ?? 0));
|
|
1586
|
+
keyValue("Failed (total)", String(it.failedCount ?? 0));
|
|
1587
|
+
keyValue("Skipped", String(it.skippedCount ?? 0));
|
|
1588
|
+
if (it.lastError) {
|
|
1589
|
+
keyValue("Last error", it.lastError);
|
|
1590
|
+
}
|
|
1591
|
+
const sample = Array.isArray(it.failedUserIdsSample)
|
|
1592
|
+
? it.failedUserIdsSample
|
|
1593
|
+
: [];
|
|
1594
|
+
if (sample.length > 0) {
|
|
1595
|
+
const label = it.failedUserIdsTruncated
|
|
1596
|
+
? `Failed user IDs (sample, truncated; ${it.failedCount} total)`
|
|
1597
|
+
: `Failed user IDs (sample)`;
|
|
1598
|
+
keyValue(label, sample.join(", "));
|
|
608
1599
|
}
|
|
609
1600
|
}
|
|
610
1601
|
catch (err) {
|
|
@@ -612,184 +1603,189 @@ Examples:
|
|
|
612
1603
|
process.exit(1);
|
|
613
1604
|
}
|
|
614
1605
|
});
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
.
|
|
618
|
-
.
|
|
619
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1606
|
+
iterations
|
|
1607
|
+
.command("reset")
|
|
1608
|
+
.description("Reset a terminal (completed/failed) iteration so the next trigger runs fresh")
|
|
1609
|
+
.argument("<name>", "Iteration name")
|
|
620
1610
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
621
|
-
.option("--name <name>", "Configuration name (required)")
|
|
622
|
-
.option("--description <desc>", "Description")
|
|
623
|
-
.option("--from-file <path>", "Load steps from TOML file")
|
|
624
1611
|
.option("--json", "Output as JSON")
|
|
625
|
-
.action(async (
|
|
1612
|
+
.action(async (name, options) => {
|
|
626
1613
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
627
|
-
if (!options.name) {
|
|
628
|
-
error("--name is required");
|
|
629
|
-
process.exit(1);
|
|
630
|
-
}
|
|
631
|
-
let steps = [];
|
|
632
|
-
if (options.fromFile) {
|
|
633
|
-
try {
|
|
634
|
-
const content = readFileSync(options.fromFile, "utf-8");
|
|
635
|
-
const tomlData = TOML.parse(content);
|
|
636
|
-
steps = tomlData.steps || [];
|
|
637
|
-
}
|
|
638
|
-
catch (err) {
|
|
639
|
-
error(`Failed to read TOML file: ${err.message}`);
|
|
640
|
-
process.exit(1);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
1614
|
const client = new ApiClient();
|
|
644
1615
|
try {
|
|
645
|
-
const
|
|
646
|
-
configName: options.name,
|
|
647
|
-
description: options.description,
|
|
648
|
-
steps,
|
|
649
|
-
});
|
|
1616
|
+
const res = await client.resetIteration(resolvedAppId, name);
|
|
650
1617
|
if (options.json) {
|
|
651
|
-
json(
|
|
1618
|
+
json(res);
|
|
652
1619
|
return;
|
|
653
1620
|
}
|
|
654
|
-
success(`
|
|
655
|
-
keyValue("Config ID", config.configId);
|
|
1621
|
+
success(`Reset iteration "${name}" (was ${res.previousStatus}). The next trigger will run fresh.`);
|
|
656
1622
|
}
|
|
657
1623
|
catch (err) {
|
|
1624
|
+
// A running iteration is refused with 409 + lock metadata — surface a
|
|
1625
|
+
// clear message rather than a raw error.
|
|
658
1626
|
error(err.message);
|
|
659
1627
|
process.exit(1);
|
|
660
1628
|
}
|
|
661
1629
|
});
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
.
|
|
666
|
-
.
|
|
667
|
-
.
|
|
668
|
-
.option("--
|
|
669
|
-
.option("--name <name>", "Configuration name")
|
|
670
|
-
.option("--description <desc>", "Description")
|
|
671
|
-
.option("--from-file <path>", "Update steps from TOML file")
|
|
1630
|
+
// Analytics overview
|
|
1631
|
+
const analyticsCmd = workflows.command("analytics").description("View workflow performance analytics");
|
|
1632
|
+
analyticsCmd
|
|
1633
|
+
.command("overview")
|
|
1634
|
+
.description("View workflow performance metrics")
|
|
1635
|
+
.option("--app <app-id>", "App ID")
|
|
1636
|
+
.option("--days <n>", "Time window in days", "7")
|
|
672
1637
|
.option("--json", "Output as JSON")
|
|
673
|
-
.action(async (
|
|
1638
|
+
.action(async (options) => {
|
|
674
1639
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
675
|
-
const payload = {};
|
|
676
|
-
if (options.name)
|
|
677
|
-
payload.configName = options.name;
|
|
678
|
-
if (options.description !== undefined)
|
|
679
|
-
payload.description = options.description;
|
|
680
|
-
if (options.fromFile) {
|
|
681
|
-
try {
|
|
682
|
-
const content = readFileSync(options.fromFile, "utf-8");
|
|
683
|
-
const tomlData = TOML.parse(content);
|
|
684
|
-
payload.steps = tomlData.steps || [];
|
|
685
|
-
}
|
|
686
|
-
catch (err) {
|
|
687
|
-
error(`Failed to read TOML file: ${err.message}`);
|
|
688
|
-
process.exit(1);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
if (Object.keys(payload).length === 0) {
|
|
692
|
-
error("No update options specified. Use --name, --description, or --from-file.");
|
|
693
|
-
process.exit(1);
|
|
694
|
-
}
|
|
695
1640
|
const client = new ApiClient();
|
|
696
1641
|
try {
|
|
697
|
-
const
|
|
1642
|
+
const result = await client.getWorkflowAnalytics(resolvedAppId, {
|
|
1643
|
+
windowDays: parseInt(options.days),
|
|
1644
|
+
});
|
|
698
1645
|
if (options.json) {
|
|
699
|
-
json(
|
|
1646
|
+
json(result);
|
|
700
1647
|
return;
|
|
701
1648
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
keyValue("
|
|
1649
|
+
info(`Workflow Analytics (${result.windowDays || options.days} days)`);
|
|
1650
|
+
console.log();
|
|
1651
|
+
keyValue("Total Runs", String(result.totals?.totalRuns ?? 0));
|
|
1652
|
+
keyValue("Success Rate", `${((result.totals?.successRate ?? 0) * 100).toFixed(1)}%`);
|
|
1653
|
+
keyValue("Avg Duration", formatDuration(result.totals?.avgDurationMs));
|
|
1654
|
+
if (result.tokenUsage) {
|
|
1655
|
+
console.log();
|
|
1656
|
+
info("Token Usage:");
|
|
1657
|
+
keyValue(" Input Tokens", String(result.tokenUsage.totalInputTokens ?? 0));
|
|
1658
|
+
keyValue(" Output Tokens", String(result.tokenUsage.totalOutputTokens ?? 0));
|
|
1659
|
+
keyValue(" Total Tokens", String(result.tokenUsage.totalTokens ?? 0));
|
|
1660
|
+
}
|
|
705
1661
|
}
|
|
706
1662
|
catch (err) {
|
|
707
1663
|
error(err.message);
|
|
708
1664
|
process.exit(1);
|
|
709
1665
|
}
|
|
710
1666
|
});
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
.
|
|
714
|
-
.
|
|
715
|
-
.
|
|
716
|
-
.
|
|
717
|
-
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
1667
|
+
analyticsCmd
|
|
1668
|
+
.command("top")
|
|
1669
|
+
.description("View top workflows by usage")
|
|
1670
|
+
.option("--app <app-id>", "App ID")
|
|
1671
|
+
.option("--days <n>", "Time window in days", "7")
|
|
1672
|
+
.option("--limit <n>", "Number of workflows to show", "10")
|
|
718
1673
|
.option("--json", "Output as JSON")
|
|
719
|
-
.action(async (
|
|
1674
|
+
.action(async (options) => {
|
|
720
1675
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
721
1676
|
const client = new ApiClient();
|
|
722
1677
|
try {
|
|
723
|
-
const result = await client.
|
|
1678
|
+
const result = await client.getTopWorkflows(resolvedAppId, {
|
|
1679
|
+
windowDays: parseInt(options.days),
|
|
1680
|
+
limit: parseInt(options.limit),
|
|
1681
|
+
});
|
|
724
1682
|
if (options.json) {
|
|
725
1683
|
json(result);
|
|
726
1684
|
return;
|
|
727
1685
|
}
|
|
728
|
-
|
|
729
|
-
|
|
1686
|
+
if (!result.items || result.items.length === 0) {
|
|
1687
|
+
info("No workflow activity in this period.");
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
console.log(formatTable(result.items, [
|
|
1691
|
+
{ header: "WORKFLOW", key: "workflow" },
|
|
1692
|
+
{ header: "RUNS", key: "executions" },
|
|
1693
|
+
{ header: "SUCCESS", key: "success_pct", format: (v) => `${v}%` },
|
|
1694
|
+
{ header: "P50", key: "p50", format: (v) => formatDuration(v) },
|
|
1695
|
+
{ header: "P95", key: "p95", format: (v) => formatDuration(v) },
|
|
1696
|
+
{ header: "TOKENS", key: "total_tokens", format: (v) => v ? String(Math.round(v)).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : "-" },
|
|
1697
|
+
]));
|
|
730
1698
|
}
|
|
731
1699
|
catch (err) {
|
|
732
1700
|
error(err.message);
|
|
733
1701
|
process.exit(1);
|
|
734
1702
|
}
|
|
735
1703
|
});
|
|
736
|
-
//
|
|
1704
|
+
// ============================================
|
|
1705
|
+
// CONFIGS SUBCOMMAND
|
|
1706
|
+
// ============================================
|
|
1707
|
+
const configs = workflows
|
|
1708
|
+
.command("configs")
|
|
1709
|
+
.description("Manage workflow configurations")
|
|
1710
|
+
.addHelpText("after", `
|
|
1711
|
+
Examples:
|
|
1712
|
+
$ primitive workflows configs list <workflow-id>
|
|
1713
|
+
$ primitive workflows configs get <workflow-id> <config-id>
|
|
1714
|
+
|
|
1715
|
+
Config bodies are authored as files, not verbs: write
|
|
1716
|
+
workflows/<key>.configs/<name>.toml and name the live one with
|
|
1717
|
+
activeConfigName in workflows/<key>.toml, then run 'primitive sync push'.
|
|
1718
|
+
`);
|
|
1719
|
+
// List configs
|
|
737
1720
|
configs
|
|
738
|
-
.command("
|
|
739
|
-
.description("
|
|
740
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
741
|
-
.argument("<config-id>", "Configuration ID to duplicate")
|
|
1721
|
+
.command("list")
|
|
1722
|
+
.description("List configurations for a workflow")
|
|
1723
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
742
1724
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
743
|
-
.option("--name <name>", "Name for the new configuration")
|
|
744
1725
|
.option("--json", "Output as JSON")
|
|
745
|
-
.action(async (workflowId,
|
|
1726
|
+
.action(async (workflowId, options) => {
|
|
746
1727
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
747
1728
|
const client = new ApiClient();
|
|
748
1729
|
try {
|
|
749
|
-
const
|
|
750
|
-
configName: options.name,
|
|
751
|
-
});
|
|
1730
|
+
const { items } = await client.listWorkflowConfigs(resolvedAppId, workflowId);
|
|
752
1731
|
if (options.json) {
|
|
753
|
-
json(
|
|
1732
|
+
json(items);
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (!items || items.length === 0) {
|
|
1736
|
+
info("No configurations found.");
|
|
754
1737
|
return;
|
|
755
1738
|
}
|
|
756
|
-
|
|
757
|
-
|
|
1739
|
+
// Get workflow to find active config
|
|
1740
|
+
const { workflow } = await client.getWorkflow(resolvedAppId, workflowId);
|
|
1741
|
+
const activeConfigId = workflow?.activeConfigId;
|
|
1742
|
+
console.log(formatTable(items, [
|
|
1743
|
+
{ header: "ID", key: "configId", format: formatId },
|
|
1744
|
+
{ header: "NAME", key: "configName" },
|
|
1745
|
+
{ header: "STATUS", key: "status", format: formatStatus },
|
|
1746
|
+
{
|
|
1747
|
+
header: "ACTIVE",
|
|
1748
|
+
key: "configId",
|
|
1749
|
+
format: (v) => v === activeConfigId ? "★" : "",
|
|
1750
|
+
},
|
|
1751
|
+
{ header: "MODIFIED", key: "modifiedAt", format: formatDate },
|
|
1752
|
+
]));
|
|
758
1753
|
}
|
|
759
1754
|
catch (err) {
|
|
760
1755
|
error(err.message);
|
|
761
1756
|
process.exit(1);
|
|
762
1757
|
}
|
|
763
1758
|
});
|
|
764
|
-
//
|
|
1759
|
+
// Get config
|
|
765
1760
|
configs
|
|
766
|
-
.command("
|
|
767
|
-
.description("
|
|
768
|
-
.argument("<workflow-id>", "Workflow ID")
|
|
1761
|
+
.command("get")
|
|
1762
|
+
.description("Get configuration details")
|
|
1763
|
+
.argument("<workflow-id>", "Workflow ID or key")
|
|
769
1764
|
.argument("<config-id>", "Configuration ID")
|
|
770
1765
|
.option("--app <app-id>", "App ID (uses current app if not specified)")
|
|
771
|
-
.option("
|
|
1766
|
+
.option("--json", "Output as JSON")
|
|
772
1767
|
.action(async (workflowId, configId, options) => {
|
|
773
1768
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
774
|
-
if (!options.yes) {
|
|
775
|
-
const inquirer = await import("inquirer");
|
|
776
|
-
const { confirm } = await inquirer.default.prompt([
|
|
777
|
-
{
|
|
778
|
-
type: "confirm",
|
|
779
|
-
name: "confirm",
|
|
780
|
-
message: `Are you sure you want to archive configuration ${configId}?`,
|
|
781
|
-
default: false,
|
|
782
|
-
},
|
|
783
|
-
]);
|
|
784
|
-
if (!confirm) {
|
|
785
|
-
info("Cancelled.");
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
1769
|
const client = new ApiClient();
|
|
790
1770
|
try {
|
|
791
|
-
await client.
|
|
792
|
-
|
|
1771
|
+
const config = await client.getWorkflowConfig(resolvedAppId, workflowId, configId);
|
|
1772
|
+
if (options.json) {
|
|
1773
|
+
json(config);
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
printResult("Config ID", config.configId);
|
|
1777
|
+
printResult("Name", config.configName);
|
|
1778
|
+
printResult("Description", config.description || "-");
|
|
1779
|
+
printResult("Status", formatStatus(config.status));
|
|
1780
|
+
printResult("Created", formatDate(config.createdAt));
|
|
1781
|
+
printResult("Modified", formatDate(config.modifiedAt));
|
|
1782
|
+
if (config.steps && config.steps.length > 0) {
|
|
1783
|
+
divider();
|
|
1784
|
+
info(`Steps (${config.steps.length}):`);
|
|
1785
|
+
config.steps.forEach((step, idx) => {
|
|
1786
|
+
console.log(` ${idx + 1}. ${step.id} (${step.kind})`);
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
793
1789
|
}
|
|
794
1790
|
catch (err) {
|
|
795
1791
|
error(err.message);
|
|
@@ -810,7 +1806,6 @@ Examples:
|
|
|
810
1806
|
$ primitive workflows tests run-all <workflow-id>
|
|
811
1807
|
$ primitive workflows tests run-all <workflow-id> --test-cases 01ABC,01DEF,01GHI
|
|
812
1808
|
`);
|
|
813
|
-
// List test cases
|
|
814
1809
|
tests
|
|
815
1810
|
.command("list")
|
|
816
1811
|
.description("List test cases for a workflow")
|
|
@@ -925,13 +1920,15 @@ Examples:
|
|
|
925
1920
|
json(result);
|
|
926
1921
|
return;
|
|
927
1922
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1923
|
+
printResult("Test Case ID", result.testCaseId);
|
|
1924
|
+
printResult("Name", result.name);
|
|
1925
|
+
printResult("Created", formatDate(result.createdAt));
|
|
931
1926
|
divider();
|
|
932
1927
|
info("Input Variables:");
|
|
933
1928
|
try {
|
|
934
|
-
const vars =
|
|
1929
|
+
const vars = typeof result.inputVariables === 'string'
|
|
1930
|
+
? JSON.parse(result.inputVariables || "{}")
|
|
1931
|
+
: (result.inputVariables || {});
|
|
935
1932
|
console.log(JSON.stringify(vars, null, 2));
|
|
936
1933
|
}
|
|
937
1934
|
catch {
|
|
@@ -939,13 +1936,16 @@ Examples:
|
|
|
939
1936
|
}
|
|
940
1937
|
if (result.expectedOutputPattern) {
|
|
941
1938
|
divider();
|
|
942
|
-
|
|
1939
|
+
printResult("Expected Pattern", result.expectedOutputPattern);
|
|
943
1940
|
}
|
|
944
1941
|
if (result.expectedOutputContains) {
|
|
945
1942
|
divider();
|
|
946
1943
|
info("Expected Contains:");
|
|
947
1944
|
try {
|
|
948
|
-
|
|
1945
|
+
const contains = typeof result.expectedOutputContains === 'string'
|
|
1946
|
+
? JSON.parse(result.expectedOutputContains)
|
|
1947
|
+
: result.expectedOutputContains;
|
|
1948
|
+
console.log(JSON.stringify(contains, null, 2));
|
|
949
1949
|
}
|
|
950
1950
|
catch {
|
|
951
1951
|
console.log(result.expectedOutputContains);
|
|
@@ -955,7 +1955,10 @@ Examples:
|
|
|
955
1955
|
divider();
|
|
956
1956
|
info("Expected JSON Subset:");
|
|
957
1957
|
try {
|
|
958
|
-
|
|
1958
|
+
const subset = typeof result.expectedJsonSubset === 'string'
|
|
1959
|
+
? JSON.parse(result.expectedJsonSubset)
|
|
1960
|
+
: result.expectedJsonSubset;
|
|
1961
|
+
console.log(JSON.stringify(subset, null, 2));
|
|
959
1962
|
}
|
|
960
1963
|
catch {
|
|
961
1964
|
console.log(result.expectedJsonSubset);
|
|
@@ -1056,15 +2059,14 @@ Examples:
|
|
|
1056
2059
|
.action(async (workflowId, testCaseId, options) => {
|
|
1057
2060
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1058
2061
|
if (!options.yes) {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
{
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
]);
|
|
2062
|
+
let confirm;
|
|
2063
|
+
try {
|
|
2064
|
+
confirm = await confirmPrompt(`Are you sure you want to delete test case ${testCaseId}?`);
|
|
2065
|
+
}
|
|
2066
|
+
catch (err) {
|
|
2067
|
+
error(err.message);
|
|
2068
|
+
process.exit(1);
|
|
2069
|
+
}
|
|
1068
2070
|
if (!confirm) {
|
|
1069
2071
|
info("Cancelled.");
|
|
1070
2072
|
return;
|
|
@@ -1334,12 +2336,21 @@ Examples:
|
|
|
1334
2336
|
let result = await fetchStatus();
|
|
1335
2337
|
if (options.wait && result.status === "running") {
|
|
1336
2338
|
info("Waiting for batch completion...");
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
2339
|
+
// #1969 — shared poll primitive (delay-then-probe every 2s, honoring
|
|
2340
|
+
// an AbortSignal). `onError: "throw"` preserves the prior loop's
|
|
2341
|
+
// fail-fast behavior: a throwing fetchStatus (expired token, deleted
|
|
2342
|
+
// batch → 401/404) propagates to the command-level catch below and
|
|
2343
|
+
// exits non-zero, rather than spinning forever with a frozen progress
|
|
2344
|
+
// line. (The follow/watch tail loops legitimately keep "continue".)
|
|
2345
|
+
const controller = new AbortController();
|
|
2346
|
+
result = await pollUntil(fetchStatus, (r) => r.status !== "running", {
|
|
2347
|
+
intervalMs: 2000,
|
|
2348
|
+
signal: controller.signal,
|
|
2349
|
+
onError: "throw",
|
|
2350
|
+
// Progress goes to stderr so it can't corrupt JSON on stdout under --json.
|
|
2351
|
+
onValue: (r) => progress(` Completed: ${r.completed}/${r.results?.length || 0} `),
|
|
2352
|
+
});
|
|
2353
|
+
progressEnd();
|
|
1343
2354
|
}
|
|
1344
2355
|
if (options.json) {
|
|
1345
2356
|
json(result);
|
|
@@ -1383,15 +2394,14 @@ Examples:
|
|
|
1383
2394
|
.action(async (workflowId, batchId, options) => {
|
|
1384
2395
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1385
2396
|
if (!options.yes) {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
{
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
]);
|
|
2397
|
+
let confirm;
|
|
2398
|
+
try {
|
|
2399
|
+
confirm = await confirmPrompt(`Are you sure you want to cancel batch ${batchId}?`);
|
|
2400
|
+
}
|
|
2401
|
+
catch (err) {
|
|
2402
|
+
error(err.message);
|
|
2403
|
+
process.exit(1);
|
|
2404
|
+
}
|
|
1395
2405
|
if (!confirm) {
|
|
1396
2406
|
info("Cancelled.");
|
|
1397
2407
|
return;
|
|
@@ -1555,15 +2565,14 @@ Examples:
|
|
|
1555
2565
|
.action(async (workflowId, testCaseId, filename, options) => {
|
|
1556
2566
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1557
2567
|
if (!options.yes) {
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
{
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
]);
|
|
2568
|
+
let confirm;
|
|
2569
|
+
try {
|
|
2570
|
+
confirm = await confirmPrompt(`Are you sure you want to delete attachment "${filename}"?`);
|
|
2571
|
+
}
|
|
2572
|
+
catch (err) {
|
|
2573
|
+
error(err.message);
|
|
2574
|
+
process.exit(1);
|
|
2575
|
+
}
|
|
1567
2576
|
if (!confirm) {
|
|
1568
2577
|
info("Cancelled.");
|
|
1569
2578
|
return;
|
|
@@ -1579,6 +2588,130 @@ Examples:
|
|
|
1579
2588
|
process.exit(1);
|
|
1580
2589
|
}
|
|
1581
2590
|
});
|
|
2591
|
+
// ============================================
|
|
2592
|
+
// Codegen command (issue #1442)
|
|
2593
|
+
// ============================================
|
|
2594
|
+
workflows
|
|
2595
|
+
.command("codegen")
|
|
2596
|
+
.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.")
|
|
2597
|
+
.argument("[workflow-key]", "Generate for a single workflow (defaults to every workflows/*.toml found)")
|
|
2598
|
+
.option("--app <app-id>", "App ID")
|
|
2599
|
+
.option("--sync-dir <path>", "Override path to the sync directory (defaults to ./.primitive/sync/<env>/<appId>/)")
|
|
2600
|
+
.option("-o, --output <dir>", "Output directory for the generated files (defaults to <sync-dir>/workflows/generated/)")
|
|
2601
|
+
.option("--lang <lang>", "Target language for the generated types: 'ts' (default) or 'swift'.", "ts")
|
|
2602
|
+
.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.")
|
|
2603
|
+
.option("--json", "Output the result summary as JSON")
|
|
2604
|
+
.action(async (workflowKey, options) => {
|
|
2605
|
+
try {
|
|
2606
|
+
// 0. Validate --lang up front so an unknown value fails clearly rather
|
|
2607
|
+
// than silently defaulting to the TS emitter.
|
|
2608
|
+
const lang = String(options.lang ?? "ts").toLowerCase();
|
|
2609
|
+
if (lang !== "ts" && lang !== "swift") {
|
|
2610
|
+
error(`Unknown --lang "${options.lang}". Use "ts" or "swift".`);
|
|
2611
|
+
process.exit(1);
|
|
2612
|
+
}
|
|
2613
|
+
// 1. Resolve the single source workflows/ directory via the shared
|
|
2614
|
+
// active-environment resolver (issue #1510). Honors --sync-dir /
|
|
2615
|
+
// --app overrides, resolves the active env
|
|
2616
|
+
// (--env → PRIMITIVE_ENV → defaultEnvironment → single-env), and
|
|
2617
|
+
// preserves the legacy per-app scan fallback in bare-dir mode.
|
|
2618
|
+
const workflowsSourceDir = resolveCodegenSourceDir({
|
|
2619
|
+
subdir: "workflows",
|
|
2620
|
+
options: { app: options.app, syncDir: options.syncDir },
|
|
2621
|
+
});
|
|
2622
|
+
// 2. Collect the source .toml files (one per workflow), filtered to a
|
|
2623
|
+
// single workflow when an argument is given. Match on the file stem;
|
|
2624
|
+
// the generator resolves the real key from `[workflow].key`.
|
|
2625
|
+
const inputs = [];
|
|
2626
|
+
for (const fileName of readdirSync(workflowsSourceDir)) {
|
|
2627
|
+
if (!fileName.endsWith(".toml"))
|
|
2628
|
+
continue;
|
|
2629
|
+
const stem = fileName.slice(0, -".toml".length);
|
|
2630
|
+
if (workflowKey && stem !== workflowKey)
|
|
2631
|
+
continue;
|
|
2632
|
+
const tomlPath = path.join(workflowsSourceDir, fileName);
|
|
2633
|
+
inputs.push({
|
|
2634
|
+
fileStem: stem,
|
|
2635
|
+
tomlPath,
|
|
2636
|
+
tomlContent: readFileSync(tomlPath, "utf-8"),
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
if (inputs.length === 0) {
|
|
2640
|
+
error(workflowKey
|
|
2641
|
+
? `No workflows/${workflowKey}.toml found under .primitive/sync/.`
|
|
2642
|
+
: "No workflows/*.toml files found to generate from.");
|
|
2643
|
+
process.exit(1);
|
|
2644
|
+
}
|
|
2645
|
+
// 3. Resolve the output directory. Default: a `generated/` subdir of
|
|
2646
|
+
// the first source dir (keeps generated output beside the schema).
|
|
2647
|
+
const outputDir = options.output
|
|
2648
|
+
? path.resolve(options.output)
|
|
2649
|
+
: path.join(path.dirname(inputs[0].tomlPath), "generated");
|
|
2650
|
+
// 4. Run codegen (or --check) with the selected emitter. Both the TS
|
|
2651
|
+
// and Swift generators return the same WriteOrCheckResult shape, so
|
|
2652
|
+
// the reporting below is emitter-agnostic. Fork 1 = A: only the
|
|
2653
|
+
// emitter differs; both read the same synced TOMLs and the same
|
|
2654
|
+
// drift-guarded schema descriptor.
|
|
2655
|
+
const codegenResult = lang === "swift"
|
|
2656
|
+
? await generateWorkflowSwiftTypes({
|
|
2657
|
+
inputs,
|
|
2658
|
+
outputDir,
|
|
2659
|
+
check: !!options.check,
|
|
2660
|
+
singleWorkflow: !!workflowKey,
|
|
2661
|
+
})
|
|
2662
|
+
: await generateWorkflowTypes({
|
|
2663
|
+
inputs,
|
|
2664
|
+
outputDir,
|
|
2665
|
+
check: !!options.check,
|
|
2666
|
+
singleWorkflow: !!workflowKey,
|
|
2667
|
+
});
|
|
2668
|
+
if (options.check) {
|
|
2669
|
+
if (codegenResult.mismatches.length > 0) {
|
|
2670
|
+
if (options.json) {
|
|
2671
|
+
json({ ok: false, mismatches: codegenResult.mismatches });
|
|
2672
|
+
}
|
|
2673
|
+
else {
|
|
2674
|
+
error(`Check failed: ${codegenResult.mismatches.length} file(s) out of date.`);
|
|
2675
|
+
for (const m of codegenResult.mismatches) {
|
|
2676
|
+
error(` ${m.reason}: ${path.relative(process.cwd(), m.filePath)}`);
|
|
2677
|
+
}
|
|
2678
|
+
info(lang === "swift"
|
|
2679
|
+
? "Run `primitive workflows codegen --lang swift` to regenerate."
|
|
2680
|
+
: "Run `primitive workflows codegen` to regenerate.");
|
|
2681
|
+
}
|
|
2682
|
+
process.exit(1);
|
|
2683
|
+
}
|
|
2684
|
+
if (options.json) {
|
|
2685
|
+
json({ ok: true, checked: codegenResult.writtenFiles.length });
|
|
2686
|
+
}
|
|
2687
|
+
else {
|
|
2688
|
+
success(`Check passed: ${codegenResult.writtenFiles.length} file(s) up to date.`);
|
|
2689
|
+
}
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
if (options.json) {
|
|
2693
|
+
json({
|
|
2694
|
+
written: codegenResult.writtenFiles,
|
|
2695
|
+
deleted: codegenResult.deletedFiles,
|
|
2696
|
+
});
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
success(`Generated ${codegenResult.writtenFiles.length} file(s)` +
|
|
2700
|
+
(codegenResult.deletedFiles.length > 0
|
|
2701
|
+
? `, deleted ${codegenResult.deletedFiles.length} stale file(s).`
|
|
2702
|
+
: "."));
|
|
2703
|
+
for (const f of codegenResult.writtenFiles) {
|
|
2704
|
+
keyValue(" wrote", path.relative(process.cwd(), f));
|
|
2705
|
+
}
|
|
2706
|
+
for (const f of codegenResult.deletedFiles) {
|
|
2707
|
+
keyValue(" deleted", path.relative(process.cwd(), f));
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
catch (err) {
|
|
2711
|
+
error(err.message);
|
|
2712
|
+
process.exit(1);
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
1582
2715
|
}
|
|
1583
2716
|
function formatFileSize(bytes) {
|
|
1584
2717
|
if (bytes < 1024)
|