@kosuke-ai/kosuke 1.0.0 → 1.0.2
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/CHANGELOG.md +66 -0
- package/README.md +198 -1
- package/dist/agent/http.js +18 -0
- package/dist/agent/summary.js +419 -0
- package/dist/commands/agent.js +22 -0
- package/dist/commands/auth.js +117 -0
- package/dist/commands/integrations.js +310 -0
- package/dist/commands/skill-runs.js +230 -0
- package/dist/commands/watch.js +167 -0
- package/dist/generated/commands/activity/list.js +73 -0
- package/dist/generated/commands/crons/artifacts-list.js +57 -0
- package/dist/generated/commands/crons/create.js +65 -0
- package/dist/generated/commands/crons/delete.js +18 -0
- package/dist/generated/commands/crons/get.js +19 -0
- package/dist/generated/commands/crons/list.js +54 -0
- package/dist/generated/commands/crons/projects-resume.js +21 -0
- package/dist/generated/commands/crons/run.js +37 -0
- package/dist/generated/commands/crons/runs-artifacts-get.js +23 -0
- package/dist/generated/commands/crons/runs-artifacts-list.js +22 -0
- package/dist/generated/commands/crons/runs-cancel.js +21 -0
- package/dist/generated/commands/crons/runs-diagnostics.js +23 -0
- package/dist/generated/commands/crons/runs-get.js +21 -0
- package/dist/generated/commands/crons/runs-list.js +58 -0
- package/dist/generated/commands/crons/runs-messages.js +21 -0
- package/dist/generated/commands/crons/update.js +73 -0
- package/dist/generated/commands/deploy/cancel.js +18 -0
- package/dist/generated/commands/deploy/list.js +33 -0
- package/dist/generated/commands/deploy/message.js +32 -0
- package/dist/generated/commands/deploy/messages.js +18 -0
- package/dist/generated/commands/deploy/queue-delete.js +19 -0
- package/dist/generated/commands/deploy/queue-list.js +19 -0
- package/dist/generated/commands/deploy/queue-move.js +31 -0
- package/dist/generated/commands/deploy/queue-resume.js +19 -0
- package/dist/generated/commands/deploy/queue-send.js +20 -0
- package/dist/generated/commands/deploy/queue-steer.js +20 -0
- package/dist/generated/commands/deploy/queue-update.js +56 -0
- package/dist/generated/commands/deploy/run.js +65 -0
- package/dist/generated/commands/deploy/set-model.js +28 -0
- package/dist/generated/commands/devices/list.js +18 -0
- package/dist/generated/commands/devices/request.js +31 -0
- package/dist/generated/commands/files/delete.js +18 -0
- package/dist/generated/commands/files/get.js +19 -0
- package/dist/generated/commands/files/list.js +25 -0
- package/dist/generated/commands/files/mkdir.js +28 -0
- package/dist/generated/commands/files/move.js +35 -0
- package/dist/generated/commands/files/put.js +92 -0
- package/dist/generated/commands/github/repositories.js +16 -0
- package/dist/generated/commands/github/status.js +16 -0
- package/dist/generated/commands/index.js +656 -0
- package/dist/generated/commands/integrations/execute.js +49 -0
- package/dist/generated/commands/integrations/tools.js +70 -0
- package/dist/generated/commands/mac/apps.js +30 -0
- package/dist/generated/commands/mac/ax-tree.js +65 -0
- package/dist/generated/commands/mac/browser-connection-check.js +69 -0
- package/dist/generated/commands/mac/browser-connections.js +30 -0
- package/dist/generated/commands/mac/browser-profiles.js +36 -0
- package/dist/generated/commands/mac/browser-sign-in.js +79 -0
- package/dist/generated/commands/mac/browser.js +44 -0
- package/dist/generated/commands/mac/click.js +72 -0
- package/dist/generated/commands/mac/file-delete.js +37 -0
- package/dist/generated/commands/mac/file-list.js +37 -0
- package/dist/generated/commands/mac/file-move.js +44 -0
- package/dist/generated/commands/mac/file-read.js +37 -0
- package/dist/generated/commands/mac/file-write.js +50 -0
- package/dist/generated/commands/mac/key.js +58 -0
- package/dist/generated/commands/mac/open.js +43 -0
- package/dist/generated/commands/mac/screenshot.js +51 -0
- package/dist/generated/commands/mac/scroll.js +70 -0
- package/dist/generated/commands/mac/shell.js +51 -0
- package/dist/generated/commands/mac/tools.js +18 -0
- package/dist/generated/commands/mac/type.js +64 -0
- package/dist/generated/commands/mac/windows.js +30 -0
- package/dist/generated/commands/members/list.js +24 -0
- package/dist/generated/commands/models/list.js +16 -0
- package/dist/generated/commands/personal-tasks/create.js +72 -0
- package/dist/generated/commands/personal-tasks/delete.js +17 -0
- package/dist/generated/commands/personal-tasks/get.js +20 -0
- package/dist/generated/commands/personal-tasks/list.js +71 -0
- package/dist/generated/commands/personal-tasks/move.js +31 -0
- package/dist/generated/commands/personal-tasks/start.js +69 -0
- package/dist/generated/commands/personal-tasks/update.js +81 -0
- package/dist/generated/commands/preview/get.js +26 -0
- package/dist/generated/commands/preview/health.js +26 -0
- package/dist/generated/commands/preview/reset.js +28 -0
- package/dist/generated/commands/preview/share-create.js +37 -0
- package/dist/generated/commands/preview/share-delete.js +26 -0
- package/dist/generated/commands/preview/share-get.js +27 -0
- package/dist/generated/commands/preview/shared-artifact.js +19 -0
- package/dist/generated/commands/preview/shared.js +17 -0
- package/dist/generated/commands/projects/backlog-items-create.js +35 -0
- package/dist/generated/commands/projects/backlog-items-delete.js +19 -0
- package/dist/generated/commands/projects/backlog-items-list.js +19 -0
- package/dist/generated/commands/projects/backlog-items-send.js +20 -0
- package/dist/generated/commands/projects/backlog-items-update.js +42 -0
- package/dist/generated/commands/projects/batch-merging-add-member.js +30 -0
- package/dist/generated/commands/projects/batch-merging-eject.js +37 -0
- package/dist/generated/commands/projects/batch-merging-finish.js +30 -0
- package/dist/generated/commands/projects/batch-merging-get.js +19 -0
- package/dist/generated/commands/projects/batch-merging-set.js +32 -0
- package/dist/generated/commands/projects/branches.js +31 -0
- package/dist/generated/commands/projects/create.js +72 -0
- package/dist/generated/commands/projects/delete.js +17 -0
- package/dist/generated/commands/projects/env-vars-add.js +22 -0
- package/dist/generated/commands/projects/env-vars-remove.js +21 -0
- package/dist/generated/commands/projects/get.js +18 -0
- package/dist/generated/commands/projects/labels-color.js +37 -0
- package/dist/generated/commands/projects/labels-delete.js +50 -0
- package/dist/generated/commands/projects/labels-usage.js +27 -0
- package/dist/generated/commands/projects/labels.js +18 -0
- package/dist/generated/commands/projects/list.js +16 -0
- package/dist/generated/commands/projects/live-logs.js +34 -0
- package/dist/generated/commands/projects/live-query.js +30 -0
- package/dist/generated/commands/projects/live-traffic.js +27 -0
- package/dist/generated/commands/projects/live-write-status.js +20 -0
- package/dist/generated/commands/projects/live-write.js +37 -0
- package/dist/generated/commands/projects/qa-guide-get.js +19 -0
- package/dist/generated/commands/projects/qa-guide-set.js +40 -0
- package/dist/generated/commands/projects/releases.js +18 -0
- package/dist/generated/commands/projects/services-add.js +88 -0
- package/dist/generated/commands/projects/services-list.js +19 -0
- package/dist/generated/commands/projects/services-remove.js +19 -0
- package/dist/generated/commands/projects/services-update.js +82 -0
- package/dist/generated/commands/projects/settings-get.js +19 -0
- package/dist/generated/commands/projects/settings-import.js +99 -0
- package/dist/generated/commands/projects/settings-update.js +85 -0
- package/dist/generated/commands/projects/skills.js +39 -0
- package/dist/generated/commands/projects/switch-workflow.js +34 -0
- package/dist/generated/commands/projects/update.js +28 -0
- package/dist/generated/commands/projects/versions.js +18 -0
- package/dist/generated/commands/sensei/cards-close.js +43 -0
- package/dist/generated/commands/sensei/cards-mark-read.js +30 -0
- package/dist/generated/commands/sensei/cards-put.js +152 -0
- package/dist/generated/commands/sensei/cards-send-reply.js +30 -0
- package/dist/generated/commands/sensei/cards.js +38 -0
- package/dist/generated/commands/sensei/chat.js +23 -0
- package/dist/generated/commands/sensei/connect.js +57 -0
- package/dist/generated/commands/sensei/delegates-get.js +21 -0
- package/dist/generated/commands/sensei/delegates-note.js +46 -0
- package/dist/generated/commands/sensei/delegates-start.js +51 -0
- package/dist/generated/commands/sensei/delegates-stop.js +21 -0
- package/dist/generated/commands/sensei/describe.js +36 -0
- package/dist/generated/commands/sensei/device-requests-decide.js +48 -0
- package/dist/generated/commands/sensei/device-requests-list.js +27 -0
- package/dist/generated/commands/sensei/memory-get.js +19 -0
- package/dist/generated/commands/sensei/memory-put.js +55 -0
- package/dist/generated/commands/sensei/merge-rule.js +36 -0
- package/dist/generated/commands/sensei/merge.js +36 -0
- package/dist/generated/commands/sensei/messages-reply.js +44 -0
- package/dist/generated/commands/sensei/messages-reroute.js +37 -0
- package/dist/generated/commands/sensei/messages-send.js +98 -0
- package/dist/generated/commands/sensei/notifications-open.js +19 -0
- package/dist/generated/commands/sensei/reels-activity.js +16 -0
- package/dist/generated/commands/sensei/reels-proposals.js +24 -0
- package/dist/generated/commands/sensei/reels-ranks.js +16 -0
- package/dist/generated/commands/sensei/reels-skips.js +32 -0
- package/dist/generated/commands/sensei/repository.js +36 -0
- package/dist/generated/commands/sensei/review-approve.js +37 -0
- package/dist/generated/commands/sensei/review-ask.js +44 -0
- package/dist/generated/commands/sensei/review-context.js +32 -0
- package/dist/generated/commands/sensei/sessions-assess.js +71 -0
- package/dist/generated/commands/sensei/sessions-facts.js +26 -0
- package/dist/generated/commands/sensei/suggest.js +36 -0
- package/dist/generated/commands/sensei/thread-done.js +21 -0
- package/dist/generated/commands/sensei/thread-move.js +30 -0
- package/dist/generated/commands/sensei/thread-read.js +41 -0
- package/dist/generated/commands/sensei/thread-split.js +30 -0
- package/dist/generated/commands/sensei/threads.js +47 -0
- package/dist/generated/commands/sensei/tickets-merge.js +51 -0
- package/dist/generated/commands/sensei/widgets.js +15 -0
- package/dist/generated/commands/sessions/activate.js +33 -0
- package/dist/generated/commands/sessions/all.js +142 -0
- package/dist/generated/commands/sessions/app-stack-start.js +20 -0
- package/dist/generated/commands/sessions/artifacts-attach.js +31 -0
- package/dist/generated/commands/sessions/artifacts-get.js +21 -0
- package/dist/generated/commands/sessions/artifacts-list.js +20 -0
- package/dist/generated/commands/sessions/create.js +234 -0
- package/dist/generated/commands/sessions/delete.js +21 -0
- package/dist/generated/commands/sessions/desktop-control.js +35 -0
- package/dist/generated/commands/sessions/desktop-start.js +19 -0
- package/dist/generated/commands/sessions/desktop.js +19 -0
- package/dist/generated/commands/sessions/diff-comments-add.js +65 -0
- package/dist/generated/commands/sessions/diff-comments-delete.js +21 -0
- package/dist/generated/commands/sessions/diff-comments-list.js +21 -0
- package/dist/generated/commands/sessions/diff-comments-update.js +25 -0
- package/dist/generated/commands/sessions/diff-commits.js +20 -0
- package/dist/generated/commands/sessions/diff-files.js +20 -0
- package/dist/generated/commands/sessions/diff-patch.js +87 -0
- package/dist/generated/commands/sessions/edit-prompt.js +81 -0
- package/dist/generated/commands/sessions/files-get.js +28 -0
- package/dist/generated/commands/sessions/files-list.js +20 -0
- package/dist/generated/commands/sessions/files-write.js +42 -0
- package/dist/generated/commands/sessions/get.js +19 -0
- package/dist/generated/commands/sessions/git-commit-message.js +20 -0
- package/dist/generated/commands/sessions/git-commit.js +30 -0
- package/dist/generated/commands/sessions/git-push.js +20 -0
- package/dist/generated/commands/sessions/git-state.js +20 -0
- package/dist/generated/commands/sessions/go-ahead-clear.js +22 -0
- package/dist/generated/commands/sessions/go-ahead-set.js +40 -0
- package/dist/generated/commands/sessions/jobs-cancel.js +21 -0
- package/dist/generated/commands/sessions/jobs-get.js +21 -0
- package/dist/generated/commands/sessions/jobs-list.js +35 -0
- package/dist/generated/commands/sessions/labels-edit.js +45 -0
- package/dist/generated/commands/sessions/labels-set.js +45 -0
- package/dist/generated/commands/sessions/list.js +130 -0
- package/dist/generated/commands/sessions/mark-read.js +19 -0
- package/dist/generated/commands/sessions/message.js +33 -0
- package/dist/generated/commands/sessions/messages.js +41 -0
- package/dist/generated/commands/sessions/needs-you.js +16 -0
- package/dist/generated/commands/sessions/qa-clear.js +22 -0
- package/dist/generated/commands/sessions/qa-set.js +33 -0
- package/dist/generated/commands/sessions/queue-delete.js +23 -0
- package/dist/generated/commands/sessions/queue-list.js +20 -0
- package/dist/generated/commands/sessions/queue-move.js +32 -0
- package/dist/generated/commands/sessions/queue-resume.js +20 -0
- package/dist/generated/commands/sessions/queue-send.js +21 -0
- package/dist/generated/commands/sessions/queue-steer.js +21 -0
- package/dist/generated/commands/sessions/queue-update.js +57 -0
- package/dist/generated/commands/sessions/runtime-command.js +39 -0
- package/dist/generated/commands/sessions/search.js +24 -0
- package/dist/generated/commands/sessions/send.js +138 -0
- package/dist/generated/commands/sessions/steer.js +36 -0
- package/dist/generated/commands/sessions/sync.js +37 -0
- package/dist/generated/commands/sessions/title-generate.js +29 -0
- package/dist/generated/commands/sessions/update.js +118 -0
- package/dist/generated/commands/tasks/all.js +112 -0
- package/dist/generated/commands/tasks/create.js +230 -0
- package/dist/generated/commands/tasks/delete.js +18 -0
- package/dist/generated/commands/tasks/get.js +19 -0
- package/dist/generated/commands/tasks/labels.js +18 -0
- package/dist/generated/commands/tasks/list.js +114 -0
- package/dist/generated/commands/tasks/start.js +68 -0
- package/dist/generated/commands/tasks/update.js +136 -0
- package/dist/generated/commands/user/auth-token-revoke.js +16 -0
- package/dist/generated/commands/user/crons-artifacts-list.js +59 -0
- package/dist/generated/commands/user/crons-create.js +65 -0
- package/dist/generated/commands/user/crons-get.js +21 -0
- package/dist/generated/commands/user/crons-list.js +41 -0
- package/dist/generated/commands/user/crons-run.js +21 -0
- package/dist/generated/commands/user/crons-runs-artifacts-get.js +25 -0
- package/dist/generated/commands/user/crons-runs-artifacts-list.js +24 -0
- package/dist/generated/commands/user/crons-runs-get.js +23 -0
- package/dist/generated/commands/user/crons-runs-list.js +52 -0
- package/dist/generated/commands/user/crons-update.js +73 -0
- package/dist/generated/commands/user/files-delete.js +18 -0
- package/dist/generated/commands/user/files-get.js +19 -0
- package/dist/generated/commands/user/files-list.js +24 -0
- package/dist/generated/commands/user/files-mkdir.js +27 -0
- package/dist/generated/commands/user/files-move.js +35 -0
- package/dist/generated/commands/user/files-put.js +91 -0
- package/dist/generated/commands/user/integrations-linkedin-connect.js +35 -0
- package/dist/generated/commands/user/integrations-linkedin-list.js +17 -0
- package/dist/generated/commands/user/integrations-linkedin-reconnect.js +19 -0
- package/dist/generated/commands/user/integrations-list.js +16 -0
- package/dist/generated/commands/user/integrations-remove.js +19 -0
- package/dist/generated/commands/user/integrations-x-connect.js +35 -0
- package/dist/generated/commands/user/integrations-x-list.js +17 -0
- package/dist/generated/commands/user/integrations-x-reconnect.js +19 -0
- package/dist/generated/commands/user/memory-list.js +16 -0
- package/dist/generated/commands/user/memory-put.js +36 -0
- package/dist/generated/commands/user/plugins-list.js +28 -0
- package/dist/generated/commands/user/plugins-update.js +29 -0
- package/dist/generated/commands/user/profile.js +16 -0
- package/dist/generated/commands/user/push-devices-register.js +76 -0
- package/dist/generated/commands/user/push-devices-remove.js +26 -0
- package/dist/generated/commands/user/skills-create.js +33 -0
- package/dist/generated/commands/user/skills-delete.js +18 -0
- package/dist/generated/commands/user/skills-get.js +19 -0
- package/dist/generated/commands/user/skills-list.js +17 -0
- package/dist/generated/commands/user/skills-update.js +35 -0
- package/dist/generated/commands/user/subscriptions-delete.js +18 -0
- package/dist/generated/commands/user/subscriptions-get.js +17 -0
- package/dist/generated/commands/user/subscriptions-grok-change.js +20 -0
- package/dist/generated/commands/user/subscriptions-grok-get.js +18 -0
- package/dist/generated/commands/user/subscriptions-login-antigravity-cancel.js +34 -0
- package/dist/generated/commands/user/subscriptions-login-antigravity-complete.js +40 -0
- package/dist/generated/commands/user/subscriptions-login-antigravity-start.js +28 -0
- package/dist/generated/commands/user/subscriptions-login-claude-complete.js +34 -0
- package/dist/generated/commands/user/subscriptions-login-claude-start.js +29 -0
- package/dist/generated/commands/user/subscriptions-login-codex-complete.js +28 -0
- package/dist/generated/commands/user/subscriptions-login-codex-start.js +29 -0
- package/dist/generated/commands/user/subscriptions-login-muse-complete.js +28 -0
- package/dist/generated/commands/user/subscriptions-login-muse-start.js +29 -0
- package/dist/generated/commands/user/subscriptions-logins-delete.js +20 -0
- package/dist/generated/commands/user/subscriptions-logins-recheck.js +19 -0
- package/dist/generated/commands/user/subscriptions-set-default-model.js +33 -0
- package/dist/generated/commands/user/subscriptions-set-key.js +35 -0
- package/dist/generated/commands/user/subscriptions-token-usage.js +17 -0
- package/dist/generated/commands/user/subscriptions-usage.js +52 -0
- package/dist/generated/commands/workspaces/access-status.js +30 -0
- package/dist/generated/commands/workspaces/ai-provider-get.js +19 -0
- package/dist/generated/commands/workspaces/ai-provider-google-delete-method.js +33 -0
- package/dist/generated/commands/workspaces/ai-provider-google-get.js +20 -0
- package/dist/generated/commands/workspaces/ai-provider-google-remove.js +19 -0
- package/dist/generated/commands/workspaces/ai-provider-google-save.js +38 -0
- package/dist/generated/commands/workspaces/ai-provider-google-select-method.js +39 -0
- package/dist/generated/commands/workspaces/ai-provider-google-usage.js +39 -0
- package/dist/generated/commands/workspaces/ai-provider-login-antigravity-cancel.js +36 -0
- package/dist/generated/commands/workspaces/ai-provider-login-antigravity-complete.js +42 -0
- package/dist/generated/commands/workspaces/ai-provider-login-antigravity-start.js +30 -0
- package/dist/generated/commands/workspaces/ai-provider-logins-recheck.js +41 -0
- package/dist/generated/commands/workspaces/ai-provider-remove.js +57 -0
- package/dist/generated/commands/workspaces/ai-provider-set.js +21 -0
- package/dist/generated/commands/workspaces/ai-provider-token-usage.js +19 -0
- package/dist/generated/commands/workspaces/ai-provider-usage-claude.js +39 -0
- package/dist/generated/commands/workspaces/ai-provider-usage-codex.js +39 -0
- package/dist/generated/commands/workspaces/ai-provider-usage-coding-plan.js +32 -0
- package/dist/generated/commands/workspaces/ai-provider-usage-deepseek.js +32 -0
- package/dist/generated/commands/workspaces/ai-provider-usage-grok.js +39 -0
- package/dist/generated/commands/workspaces/ai-provider-usage-kimi-code.js +32 -0
- package/dist/generated/commands/workspaces/ai-provider-usage-muse.js +39 -0
- package/dist/generated/commands/workspaces/artifacts.js +26 -0
- package/dist/generated/commands/workspaces/credits.js +18 -0
- package/dist/generated/commands/workspaces/get.js +18 -0
- package/dist/generated/commands/workspaces/grok-change.js +21 -0
- package/dist/generated/commands/workspaces/grok-get.js +19 -0
- package/dist/generated/commands/workspaces/integrations-get.js +20 -0
- package/dist/generated/commands/workspaces/integrations-list.js +37 -0
- package/dist/generated/commands/workspaces/integrations-remove.js +19 -0
- package/dist/generated/commands/workspaces/list.js +16 -0
- package/dist/generated/commands/workspaces/members.js +26 -0
- package/dist/generated/commands/workspaces/plugins-list.js +30 -0
- package/dist/generated/commands/workspaces/plugins-update.js +30 -0
- package/dist/generated/commands/workspaces/provider-login-claude-complete.js +35 -0
- package/dist/generated/commands/workspaces/provider-login-claude-start.js +30 -0
- package/dist/generated/commands/workspaces/provider-login-codex-complete.js +29 -0
- package/dist/generated/commands/workspaces/provider-login-codex-start.js +30 -0
- package/dist/generated/commands/workspaces/provider-login-muse-complete.js +29 -0
- package/dist/generated/commands/workspaces/provider-login-muse-start.js +30 -0
- package/dist/generated/commands/workspaces/sandbox-limit.js +18 -0
- package/dist/generated/commands/workspaces/skills-create.js +35 -0
- package/dist/generated/commands/workspaces/skills-delete.js +19 -0
- package/dist/generated/commands/workspaces/skills-get.js +20 -0
- package/dist/generated/commands/workspaces/skills-list.js +19 -0
- package/dist/generated/commands/workspaces/skills-update.js +36 -0
- package/dist/generated/commands/workspaces/stop-sandboxes.js +28 -0
- package/dist/generated/commands/workspaces/timezone-set.js +29 -0
- package/dist/generated/commands/workspaces/usage.js +33 -0
- package/dist/generated/providers.js +31 -0
- package/dist/index.js +7 -0
- package/dist/integrations/api.js +136 -0
- package/dist/integrations/render.js +59 -0
- package/dist/program.js +73 -0
- package/dist/runtime/agent-tokens.js +39 -0
- package/dist/runtime/attach.js +199 -0
- package/dist/runtime/attachments.js +63 -0
- package/dist/runtime/client.js +188 -0
- package/dist/runtime/config.js +49 -0
- package/dist/runtime/credentials.js +32 -0
- package/dist/runtime/descriptor.js +7 -0
- package/dist/runtime/invoke.js +152 -0
- package/dist/runtime/job-watch.js +374 -0
- package/dist/runtime/runtime.js +46 -0
- package/dist/runtime/version.js +5 -0
- package/openapi.json +26313 -0
- package/package.json +46 -4
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { appendAttachments } from './attachments.js';
|
|
3
|
+
export class UsageError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export function buildRequest(operation, input) {
|
|
6
|
+
const queryNames = new Set(operation.query.map((field) => field.name));
|
|
7
|
+
const query = {};
|
|
8
|
+
for (const field of operation.query) {
|
|
9
|
+
const value = input.fields[field.name];
|
|
10
|
+
if (value !== undefined)
|
|
11
|
+
query[field.name] = value;
|
|
12
|
+
}
|
|
13
|
+
for (const field of operation.query) {
|
|
14
|
+
if (field.required && query[field.name] === undefined) {
|
|
15
|
+
throw new UsageError(`--${field.flag} is required.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const bodyFields = Object.fromEntries(Object.entries(input.fields).filter(([name, value]) => !queryNames.has(name) && value !== undefined));
|
|
19
|
+
return {
|
|
20
|
+
method: operation.method,
|
|
21
|
+
path: operation.path,
|
|
22
|
+
pathParams: input.pathParams,
|
|
23
|
+
query,
|
|
24
|
+
body: buildBody(operation, bodyFields, input),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Which media type to send. Multipart when the operation only takes that, when
|
|
29
|
+
* files are attached, or when a field given exists only on the form branch;
|
|
30
|
+
* JSON otherwise, including an empty `{}` for a body-taking operation given
|
|
31
|
+
* nothing, which is how the deploy route reads "start a deploy".
|
|
32
|
+
*/
|
|
33
|
+
function buildBody(operation, fields, input) {
|
|
34
|
+
const takesJson = operation.body !== null;
|
|
35
|
+
const takesMultipart = operation.multipart !== null;
|
|
36
|
+
if (!takesJson && !takesMultipart) {
|
|
37
|
+
if (input.wholeBody !== undefined || Object.keys(fields).length || input.attachments.length) {
|
|
38
|
+
throw new UsageError(`kosuke ${operation.command.join(' ')} takes no body.`);
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (input.attachments.length && !operation.attachments) {
|
|
43
|
+
throw new UsageError(`kosuke ${operation.command.join(' ')} takes no attachments.`);
|
|
44
|
+
}
|
|
45
|
+
// The keys that decide the media type come from --body as much as from the
|
|
46
|
+
// flags: `--body '{"content":"hi"}'` on the deploy route names a form-only
|
|
47
|
+
// field, and sending it as JSON would have zod strip it and the handler read
|
|
48
|
+
// an empty body, which there means "start a fresh deploy".
|
|
49
|
+
const jsonFields = operation.body?.fields ?? null;
|
|
50
|
+
const jsonNames = new Set(jsonFields?.map((field) => field.name) ?? []);
|
|
51
|
+
// A field flag always names a field of one branch, so it decides on its
|
|
52
|
+
// own. The keys of --body decide only against a flat JSON body: a non-flat
|
|
53
|
+
// one (an array, a union) has no field names to hold them against, and
|
|
54
|
+
// sending such a --body as the form would be a guess. No operation has that
|
|
55
|
+
// shape beside a form branch today; if one does, --body stays JSON there.
|
|
56
|
+
const given = [
|
|
57
|
+
...Object.keys(fields),
|
|
58
|
+
...(jsonFields ? Object.keys(asRecordOrEmpty(input.wholeBody)) : []),
|
|
59
|
+
];
|
|
60
|
+
const multipartOnly = given.some((name) => !jsonNames.has(name));
|
|
61
|
+
const useMultipart = takesMultipart && (!takesJson || input.attachments.length > 0 || multipartOnly);
|
|
62
|
+
if (useMultipart) {
|
|
63
|
+
const form = new FormData();
|
|
64
|
+
const source = input.wholeBody !== undefined ? asRecord(input.wholeBody, 'The body') : fields;
|
|
65
|
+
const known = new Map((operation.multipart ?? []).map((field) => [field.name, field]));
|
|
66
|
+
for (const [name, value] of Object.entries(source)) {
|
|
67
|
+
if (!known.has(name))
|
|
68
|
+
throw new UsageError(`Unknown form field "${name}".`);
|
|
69
|
+
if (value === undefined || value === null)
|
|
70
|
+
continue;
|
|
71
|
+
form.set(name, formValue(value));
|
|
72
|
+
}
|
|
73
|
+
for (const field of operation.multipart ?? []) {
|
|
74
|
+
if (field.required && !form.has(field.name)) {
|
|
75
|
+
throw new UsageError(`--${field.flag} is required.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (input.attachments.length && operation.attachments)
|
|
79
|
+
appendAttachments(form, input.attachments, operation.attachments);
|
|
80
|
+
return { kind: 'multipart', value: form };
|
|
81
|
+
}
|
|
82
|
+
if (input.wholeBody !== undefined)
|
|
83
|
+
return { kind: 'json', value: input.wholeBody };
|
|
84
|
+
if (operation.body?.fields === null) {
|
|
85
|
+
if (Object.keys(fields).length) {
|
|
86
|
+
throw new UsageError(`This body is not a flat object; pass it whole with --body, --body-file or on stdin.`);
|
|
87
|
+
}
|
|
88
|
+
if (operation.body.required) {
|
|
89
|
+
throw new UsageError(`A body is required: pass --body, or --body-file with a path or - for stdin.`);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
for (const field of operation.body?.fields ?? []) {
|
|
94
|
+
if (field.required && fields[field.name] === undefined) {
|
|
95
|
+
throw new UsageError(`--${field.flag} is required.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { kind: 'json', value: fields };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A form field is a string. A value already parsed as JSON (an object from a
|
|
102
|
+
* `json` flag, or from --body) goes back to its text, which is what the
|
|
103
|
+
* handler JSON-parses on the form branch; scalars go as they print.
|
|
104
|
+
*/
|
|
105
|
+
function formValue(value) {
|
|
106
|
+
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
107
|
+
}
|
|
108
|
+
function asRecordOrEmpty(value) {
|
|
109
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
110
|
+
? value
|
|
111
|
+
: {};
|
|
112
|
+
}
|
|
113
|
+
function asRecord(value, what) {
|
|
114
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
throw new UsageError(`${what} must be a JSON object for a multipart request.`);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* `--body '<json>'`, or `--body-file <path>` with `-` for stdin. Stdin is read
|
|
121
|
+
* only when asked for: a CLI that reads an inherited pipe on its own hangs
|
|
122
|
+
* whenever the parent never closes it, which is a bug that only shows up in
|
|
123
|
+
* someone else's script.
|
|
124
|
+
*/
|
|
125
|
+
export function readWholeBody(options, readStdin = () => readFileSync(0, 'utf8')) {
|
|
126
|
+
let text = null;
|
|
127
|
+
if (options.body !== undefined)
|
|
128
|
+
text = options.body;
|
|
129
|
+
else if (options.bodyFile === '-')
|
|
130
|
+
text = readStdin();
|
|
131
|
+
else if (options.bodyFile !== undefined)
|
|
132
|
+
text = readFileSync(options.bodyFile, 'utf8');
|
|
133
|
+
if (text === null || text.trim() === '')
|
|
134
|
+
return undefined;
|
|
135
|
+
try {
|
|
136
|
+
return JSON.parse(text);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new UsageError('The body is not valid JSON.');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export async function execute(client, request) {
|
|
143
|
+
const init = {
|
|
144
|
+
params: { path: request.pathParams, query: request.query },
|
|
145
|
+
};
|
|
146
|
+
if (request.body)
|
|
147
|
+
init.body = request.body.value;
|
|
148
|
+
// `request()` rather than the typed verbs: the descriptor decides the path at
|
|
149
|
+
// run time, and openapi-fetch's per-path types cannot follow a variable.
|
|
150
|
+
const { data, error, response } = await client.request(request.method, request.path, init);
|
|
151
|
+
return { ok: response.ok, status: response.status, body: response.ok ? data : error };
|
|
152
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Following one job's `/ws/jobs` stream to its end: the socket, the resume
|
|
3
|
+
* cursor, the close codes and the status fallback, with everything that
|
|
4
|
+
* touches the process or the network handed in, so each path is testable
|
|
5
|
+
* without either. `kosuke sessions watch` is its one caller.
|
|
6
|
+
*/
|
|
7
|
+
export const JOB_TYPES = ['chat', 'deploy'];
|
|
8
|
+
export const WATCH_EXIT = {
|
|
9
|
+
ended: 0,
|
|
10
|
+
failed: 1,
|
|
11
|
+
unauthorized: 3,
|
|
12
|
+
notFound: 4,
|
|
13
|
+
// What coreutils `timeout` exits with, so one check in a script reads both.
|
|
14
|
+
timedOut: 124,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The statuses a job never leaves, and whether each counts as a success. The
|
|
18
|
+
* two awaiting ones are a turn that ended by asking the member a question: the
|
|
19
|
+
* worker closes that stream with a successful `done`, so the fallback agrees.
|
|
20
|
+
*/
|
|
21
|
+
const ENDINGS = {
|
|
22
|
+
chat: new Map([
|
|
23
|
+
['completed', true],
|
|
24
|
+
['awaiting_response', true],
|
|
25
|
+
['failed', false],
|
|
26
|
+
['cancelled', false],
|
|
27
|
+
]),
|
|
28
|
+
deploy: new Map([
|
|
29
|
+
['deployed', true],
|
|
30
|
+
['awaiting_user', true],
|
|
31
|
+
['failed', false],
|
|
32
|
+
['cancelled', false],
|
|
33
|
+
]),
|
|
34
|
+
};
|
|
35
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
36
|
+
/**
|
|
37
|
+
* How long a job status that says it ended waits for the stream to say so too,
|
|
38
|
+
* counted from the stream's last frame so a replay is not cut off halfway.
|
|
39
|
+
* The worker writes the status before it publishes `done`, so ending on the
|
|
40
|
+
* status at once could cut off the last frames of a turn still being delivered.
|
|
41
|
+
* A cancelled chat turn publishes no `done` at all, so this is its only ending.
|
|
42
|
+
*/
|
|
43
|
+
const SETTLE_GRACE_MS = 3_000;
|
|
44
|
+
/**
|
|
45
|
+
* Without --timeout, the most a watch spends reconnecting to read the stream to
|
|
46
|
+
* its end once the status says the job ended. A stream that cannot be reached
|
|
47
|
+
* must not hold it forever, so past this it ends on the status, incomplete.
|
|
48
|
+
*/
|
|
49
|
+
const SETTLE_LIMIT_MS = 15_000;
|
|
50
|
+
/** How long one platform read may take; fetch alone would wait minutes. */
|
|
51
|
+
const READ_LIMIT_MS = 10_000;
|
|
52
|
+
const RECONNECT_BASE_MS = 1_000;
|
|
53
|
+
const RECONNECT_MAX_MS = 30_000;
|
|
54
|
+
/**
|
|
55
|
+
* Follow one job to its end and resolve with the exit code. Stdout gets frames
|
|
56
|
+
* and nothing else: the platform's own, then `watch.status` when the watched
|
|
57
|
+
* turn's own status rather than the stream ended the watch, then `watch.links`
|
|
58
|
+
* whenever the job ended. Everything a person reads goes to stderr.
|
|
59
|
+
*/
|
|
60
|
+
export function watchJob(options, deps) {
|
|
61
|
+
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
62
|
+
const settleGraceMs = options.settleGraceMs ?? SETTLE_GRACE_MS;
|
|
63
|
+
const deadline = options.timeoutMs ? Date.now() + options.timeoutMs : null;
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
// Redis stream ids: the server reads strictly after this, so a reconnect
|
|
66
|
+
// carrying the last id seen neither repeats a frame nor skips one.
|
|
67
|
+
let cursor = '0';
|
|
68
|
+
let generation = 0;
|
|
69
|
+
let socket = null;
|
|
70
|
+
let connected = false;
|
|
71
|
+
let failures = 0;
|
|
72
|
+
let finished = false;
|
|
73
|
+
let lastPollProblem = null;
|
|
74
|
+
let polling = false;
|
|
75
|
+
let nextPoll = null;
|
|
76
|
+
let reconnect = null;
|
|
77
|
+
// Why a deploy's stream refused the watch after its turn released it.
|
|
78
|
+
let streamGone = null;
|
|
79
|
+
// How the watch ends once the status says the job ended, unless the stream
|
|
80
|
+
// ends it first.
|
|
81
|
+
let ending = null;
|
|
82
|
+
let quiet = null;
|
|
83
|
+
const timers = new Set();
|
|
84
|
+
const reads = new Set();
|
|
85
|
+
const later = (ms, run) => {
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
timers.delete(timer);
|
|
88
|
+
run();
|
|
89
|
+
}, ms);
|
|
90
|
+
timers.add(timer);
|
|
91
|
+
return timer;
|
|
92
|
+
};
|
|
93
|
+
const cancel = (timer) => {
|
|
94
|
+
if (timer === null)
|
|
95
|
+
return;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
timers.delete(timer);
|
|
98
|
+
};
|
|
99
|
+
/** Settles with `read`, or rejects with `why` once `ms` pass, aborting its request. */
|
|
100
|
+
const bounded = (ms, why, read) => {
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
reads.add(controller);
|
|
103
|
+
const timer = setTimeout(() => controller.abort(new Error(why)), ms);
|
|
104
|
+
const abandoned = new Promise((_, reject) => {
|
|
105
|
+
controller.signal.addEventListener('abort', () => reject(controller.signal.reason), { once: true });
|
|
106
|
+
});
|
|
107
|
+
return Promise.race([read(controller.signal), abandoned]).finally(() => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
reads.delete(controller);
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
const end = async (code, jobEnded) => {
|
|
113
|
+
if (finished)
|
|
114
|
+
return;
|
|
115
|
+
finished = true;
|
|
116
|
+
for (const timer of timers)
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
timers.clear();
|
|
119
|
+
// A reason that is not an AbortError, which the client's retry would retry.
|
|
120
|
+
for (const read of reads)
|
|
121
|
+
read.abort(new Error('the watch ended'));
|
|
122
|
+
socket?.close(1000, 'watch ended');
|
|
123
|
+
socket = null;
|
|
124
|
+
if (jobEnded) {
|
|
125
|
+
// Clearing the timers took the timeout with them, so the links get
|
|
126
|
+
// what was left of it.
|
|
127
|
+
const left = deadline === null ? READ_LIMIT_MS : deadline - Date.now();
|
|
128
|
+
const why = left < READ_LIMIT_MS ? 'the timeout passed' : `no answer after ${READ_LIMIT_MS / 1000}s`;
|
|
129
|
+
try {
|
|
130
|
+
const links = await bounded(Math.max(0, Math.min(left, READ_LIMIT_MS)), why, deps.readLinks);
|
|
131
|
+
deps.stdout(JSON.stringify({ type: 'watch.links', data: links }));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
deps.stderr(`kosuke: could not read the links: ${describe(error)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
resolve(code);
|
|
138
|
+
};
|
|
139
|
+
/** End on the status. `gap` says why the stream could not be read to its end. */
|
|
140
|
+
const conclude = (gap) => {
|
|
141
|
+
if (finished || ending === null)
|
|
142
|
+
return;
|
|
143
|
+
if (gap !== null) {
|
|
144
|
+
deps.stderr(`kosuke: could not read the stream after frame ${cursor} (${gap}); frames after it may be missing.`);
|
|
145
|
+
}
|
|
146
|
+
if (ending.status) {
|
|
147
|
+
const data = { ...ending.status, complete: gap === null };
|
|
148
|
+
deps.stdout(JSON.stringify({ type: 'watch.status', data }));
|
|
149
|
+
}
|
|
150
|
+
if (ending.note)
|
|
151
|
+
deps.stderr(ending.note);
|
|
152
|
+
void end(ending.code, true);
|
|
153
|
+
};
|
|
154
|
+
const awaitQuiet = () => {
|
|
155
|
+
cancel(quiet);
|
|
156
|
+
quiet = later(settleGraceMs, () => conclude(null));
|
|
157
|
+
};
|
|
158
|
+
// The stream still holds what was published after the cursor, so the watch
|
|
159
|
+
// reconnects until its replay drains, bounded by --timeout or else the
|
|
160
|
+
// settle limit. The backoff starts over, so a stream that comes back soon
|
|
161
|
+
// is read before that bound passes.
|
|
162
|
+
const beginEnding = (next) => {
|
|
163
|
+
ending = next;
|
|
164
|
+
failures = 0;
|
|
165
|
+
if (deadline === null) {
|
|
166
|
+
later(SETTLE_LIMIT_MS, () => conclude(`${SETTLE_LIMIT_MS / 1000}s passed after the job ended`));
|
|
167
|
+
}
|
|
168
|
+
if (connected) {
|
|
169
|
+
awaitQuiet();
|
|
170
|
+
}
|
|
171
|
+
else if (streamGone !== null) {
|
|
172
|
+
conclude(streamGone);
|
|
173
|
+
}
|
|
174
|
+
else if (socket === null) {
|
|
175
|
+
cancel(reconnect);
|
|
176
|
+
deps.stderr(`kosuke: reconnecting after frame ${cursor} now, to read what the stream holds.`);
|
|
177
|
+
connect();
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const onFrame = (text) => {
|
|
181
|
+
let frame;
|
|
182
|
+
try {
|
|
183
|
+
frame = JSON.parse(text);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
deps.stderr('kosuke: skipped a frame that is not JSON.');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!isRecord(frame)) {
|
|
190
|
+
deps.stderr('kosuke: skipped a frame that is not a JSON object.');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// The platform's own text; one spread over lines is compacted so it
|
|
194
|
+
// still reads as one line to `jq`.
|
|
195
|
+
deps.stdout(/[\r\n]/.test(text) ? JSON.stringify(frame) : text);
|
|
196
|
+
if (typeof frame.id === 'string')
|
|
197
|
+
cursor = frame.id;
|
|
198
|
+
const data = isRecord(frame.data) ? frame.data : {};
|
|
199
|
+
if (frame.type === 'done') {
|
|
200
|
+
void end(data.success === false ? WATCH_EXIT.failed : WATCH_EXIT.ended, true);
|
|
201
|
+
}
|
|
202
|
+
else if (frame.type === 'error' && data.recoverable !== true) {
|
|
203
|
+
// A deploy reports a recoverable error and carries on to its own `done`.
|
|
204
|
+
void end(WATCH_EXIT.failed, true);
|
|
205
|
+
}
|
|
206
|
+
else if (ending) {
|
|
207
|
+
awaitQuiet();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const onClose = (code, reason) => {
|
|
211
|
+
socket = null;
|
|
212
|
+
connected = false;
|
|
213
|
+
cancel(quiet);
|
|
214
|
+
const why = `${code} ${reason || 'no reason given'}`;
|
|
215
|
+
// /ws/jobs matches a deploy by the stream its turn holds, which the worker
|
|
216
|
+
// releases as the turn ends. A stream that has delivered frames existed,
|
|
217
|
+
// so its 4404 means the turn is over, and the status says how it ended.
|
|
218
|
+
if (code === 4404 && options.jobType === 'deploy' && cursor !== '0') {
|
|
219
|
+
streamGone = why;
|
|
220
|
+
if (ending)
|
|
221
|
+
return conclude(why);
|
|
222
|
+
deps.stderr(`kosuke: the stream closed (${why}); reading the Deploy's status.`);
|
|
223
|
+
pollNow();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const refused = refusalExit(code);
|
|
227
|
+
if (refused !== null) {
|
|
228
|
+
deps.stderr(`kosuke: the platform refused the stream: ${why}`);
|
|
229
|
+
void end(refused, false);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
failures += 1;
|
|
233
|
+
const delay = Math.min(RECONNECT_BASE_MS * 2 ** (failures - 1), RECONNECT_MAX_MS);
|
|
234
|
+
deps.stderr(`kosuke: the stream closed (${why}); reconnecting after frame ${cursor} in ${delay / 1000}s.`);
|
|
235
|
+
reconnect = later(delay, connect);
|
|
236
|
+
};
|
|
237
|
+
const connect = () => {
|
|
238
|
+
const mine = ++generation;
|
|
239
|
+
// A socket this watch has moved on from, or finished with, is not heard.
|
|
240
|
+
const current = () => !finished && mine === generation;
|
|
241
|
+
let opened = null;
|
|
242
|
+
try {
|
|
243
|
+
opened = deps.connect(streamUrl(options, cursor), {
|
|
244
|
+
open: () => {
|
|
245
|
+
if (!current())
|
|
246
|
+
return;
|
|
247
|
+
failures = 0;
|
|
248
|
+
connected = true;
|
|
249
|
+
opened?.send(JSON.stringify({ type: 'auth', token: options.apiKey }));
|
|
250
|
+
if (ending)
|
|
251
|
+
awaitQuiet();
|
|
252
|
+
},
|
|
253
|
+
message: (text) => {
|
|
254
|
+
if (current())
|
|
255
|
+
onFrame(text);
|
|
256
|
+
},
|
|
257
|
+
close: (code, reason) => {
|
|
258
|
+
if (current())
|
|
259
|
+
onClose(code, reason);
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
socket = opened;
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
deps.stderr(`kosuke: could not open the stream: ${describe(error)}`);
|
|
266
|
+
void end(WATCH_EXIT.failed, false);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const schedulePoll = () => {
|
|
270
|
+
nextPoll = later(pollIntervalMs, () => void poll());
|
|
271
|
+
};
|
|
272
|
+
const pollNow = () => {
|
|
273
|
+
if (polling || ending)
|
|
274
|
+
return;
|
|
275
|
+
cancel(nextPoll);
|
|
276
|
+
void poll();
|
|
277
|
+
};
|
|
278
|
+
const poll = async () => {
|
|
279
|
+
polling = true;
|
|
280
|
+
let reading;
|
|
281
|
+
try {
|
|
282
|
+
reading = await bounded(READ_LIMIT_MS, `no answer after ${READ_LIMIT_MS / 1000}s`, deps.checkStatus);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
reading = { ok: false, httpStatus: null, message: describe(error) };
|
|
286
|
+
}
|
|
287
|
+
polling = false;
|
|
288
|
+
if (finished)
|
|
289
|
+
return;
|
|
290
|
+
if (!reading.ok) {
|
|
291
|
+
const refused = reading.httpStatus === 401 || reading.httpStatus === 403
|
|
292
|
+
? WATCH_EXIT.unauthorized
|
|
293
|
+
: reading.httpStatus === 404
|
|
294
|
+
? WATCH_EXIT.notFound
|
|
295
|
+
: null;
|
|
296
|
+
if (refused !== null) {
|
|
297
|
+
deps.stderr(`kosuke: could not read the job: ${reading.message}`);
|
|
298
|
+
void end(refused, false);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// Once per distinct problem: an outage would otherwise print every 5 s.
|
|
302
|
+
if (reading.message !== lastPollProblem) {
|
|
303
|
+
deps.stderr(`kosuke: could not read the job status (${reading.message}); still watching.`);
|
|
304
|
+
}
|
|
305
|
+
lastPollProblem = reading.message;
|
|
306
|
+
schedulePoll();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
lastPollProblem = null;
|
|
310
|
+
if ('movedOn' in reading) {
|
|
311
|
+
beginEnding({
|
|
312
|
+
code: WATCH_EXIT.failed,
|
|
313
|
+
status: null,
|
|
314
|
+
note: `kosuke: ${reading.movedOn}, so how this turn ended cannot be read.`,
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const success = ENDINGS[options.jobType].get(reading.status);
|
|
319
|
+
if (success === undefined) {
|
|
320
|
+
schedulePoll();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const { status, error, errorCode } = reading;
|
|
324
|
+
beginEnding({
|
|
325
|
+
code: success ? WATCH_EXIT.ended : WATCH_EXIT.failed,
|
|
326
|
+
status: { status, error, errorCode },
|
|
327
|
+
note: null,
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
if (options.timeoutMs) {
|
|
331
|
+
const seconds = options.timeoutMs / 1000;
|
|
332
|
+
later(options.timeoutMs, () => {
|
|
333
|
+
// The job did end: the timeout only cut short reading its stream.
|
|
334
|
+
if (ending)
|
|
335
|
+
return conclude('the timeout passed');
|
|
336
|
+
deps.stderr(`kosuke: stopped waiting after ${seconds}s; the job had not ended.`);
|
|
337
|
+
void end(WATCH_EXIT.timedOut, false);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
connect();
|
|
341
|
+
schedulePoll();
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* 4400-4499 is the server's permanent refusal: bad params, a refused token, no
|
|
346
|
+
* access, nothing to watch. Retrying any of them would loop forever.
|
|
347
|
+
*/
|
|
348
|
+
function refusalExit(code) {
|
|
349
|
+
if (code < 4400 || code > 4499)
|
|
350
|
+
return null;
|
|
351
|
+
if (code === 4401 || code === 4403)
|
|
352
|
+
return WATCH_EXIT.unauthorized;
|
|
353
|
+
if (code === 4404)
|
|
354
|
+
return WATCH_EXIT.notFound;
|
|
355
|
+
return WATCH_EXIT.failed;
|
|
356
|
+
}
|
|
357
|
+
function streamUrl(options, lastId) {
|
|
358
|
+
const url = new URL('/ws/jobs', options.apiUrl);
|
|
359
|
+
url.protocol = url.protocol === 'http:' ? 'ws:' : 'wss:';
|
|
360
|
+
url.search = new URLSearchParams({
|
|
361
|
+
projectId: options.projectId,
|
|
362
|
+
sessionId: options.sessionId,
|
|
363
|
+
jobType: options.jobType,
|
|
364
|
+
jobId: options.jobId,
|
|
365
|
+
lastId,
|
|
366
|
+
}).toString();
|
|
367
|
+
return url.toString();
|
|
368
|
+
}
|
|
369
|
+
export function isRecord(value) {
|
|
370
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
371
|
+
}
|
|
372
|
+
export function describe(error) {
|
|
373
|
+
return error instanceof Error ? error.message : String(error);
|
|
374
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createApiClient } from './client.js';
|
|
2
|
+
import { resolveCredentials, } from './credentials.js';
|
|
3
|
+
export function createRuntime({ flags, env = process.env, fetch, stdout = (line) => process.stdout.write(line + '\n'), stderr = (line) => process.stderr.write(line + '\n'), exit = (code) => {
|
|
4
|
+
process.exitCode = code;
|
|
5
|
+
}, }) {
|
|
6
|
+
let resolved = null;
|
|
7
|
+
const credentials = () => (resolved ??= resolveCredentials(flags, env));
|
|
8
|
+
return {
|
|
9
|
+
credentials,
|
|
10
|
+
client({ anonymous = false, answerTimeoutMs } = {}) {
|
|
11
|
+
const { apiKey, apiUrl } = credentials();
|
|
12
|
+
if (!apiKey && !anonymous) {
|
|
13
|
+
stderr('No API key. Run `kosuke auth login`, set KOSUKE_API_KEY, or pass --api-key. ' +
|
|
14
|
+
'Create one in Settings on the platform.');
|
|
15
|
+
exit(1);
|
|
16
|
+
throw new ExitSignal();
|
|
17
|
+
}
|
|
18
|
+
return createApiClient({
|
|
19
|
+
apiUrl,
|
|
20
|
+
apiKey: anonymous ? null : apiKey,
|
|
21
|
+
fetch,
|
|
22
|
+
answerTimeoutMs,
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
output(outcome) {
|
|
26
|
+
if (outcome.ok) {
|
|
27
|
+
// 204 has nothing to print; anything else is passed through as JSON so
|
|
28
|
+
// `| jq` works without the platform CLI knowing the shape.
|
|
29
|
+
if (outcome.body !== undefined)
|
|
30
|
+
stdout(render(outcome.body));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
stderr(render(outcome.body ?? { error: `HTTP ${outcome.status}` }));
|
|
34
|
+
exit(1);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function render(body) {
|
|
39
|
+
return typeof body === 'string' ? body : JSON.stringify(body, null, 2);
|
|
40
|
+
}
|
|
41
|
+
/** Thrown after `exit` so the action stops without printing a second error. */
|
|
42
|
+
export class ExitSignal extends Error {
|
|
43
|
+
constructor() {
|
|
44
|
+
super('exit');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
// Read at run time rather than imported with an attribute: the package's own
|
|
3
|
+
// manifest ships with it, and this works the same from src/ and from dist/.
|
|
4
|
+
const manifest = createRequire(import.meta.url)('../../package.json');
|
|
5
|
+
export const version = manifest.version;
|