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