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