@moondeck/station 0.0.1 → 1.0.0
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/LICENSE +21 -0
- package/README.md +149 -2
- package/bin/uai-host.mjs +130 -0
- package/db/migrations/0000_host_tasks.sql +12 -0
- package/db/migrations/0001_host_ui.sql +11 -0
- package/db/migrations/0002_host_github_tokens.sql +8 -0
- package/db/migrations/0003_host_ssh_keys.sql +8 -0
- package/db/migrations/0004_host_owner_name.sql +1 -0
- package/db/migrations/0005_host_env_vars.sql +5 -0
- package/db/migrations/0006_host_project_env.sql +9 -0
- package/db/migrations/0007_host_github_token_kind.sql +7 -0
- package/db/migrations/0008_host_mcp_connections.sql +18 -0
- package/db/migrations/0009_host_agent_sessions.sql +15 -0
- package/db/migrations/0010_host_engine_accounts.sql +13 -0
- package/db/migrations/0011_codex_thread_id.sql +5 -0
- package/db/migrations/0012_agent_session_attach_key.sql +4 -0
- package/db/migrations/0013_github_credential_generations.sql +11 -0
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/0017_engine_account_scope.sql +7 -0
- package/db/migrations/0018_engine_account_position.sql +3 -0
- package/db/migrations/0019_engine_account_store_ref.sql +3 -0
- package/db/migrations/0020_user_credential_store_ref.sql +6 -0
- package/db/migrations/0021_project_env_store_ref.sql +3 -0
- package/db/migrations/0022_task_project_ids.sql +5 -0
- package/db/migrations/0023_project_env_org.sql +4 -0
- package/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/0029_task_shared_files_mode.sql +5 -0
- package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
- package/db/migrations/meta/_journal.json +223 -0
- package/db/schema.ts +850 -0
- package/images/standard/Dockerfile +347 -0
- package/images/standard/README.md +167 -0
- package/images/standard/container/code-server-settings.json +63 -0
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +704 -0
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/images/standard/tool-versions +2 -0
- package/lib/__fixtures__/skill-index/SKILL.md +10 -0
- package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
- package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
- package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
- package/lib/agent-cli.ts +1507 -0
- package/lib/agent-errors.ts +22 -0
- package/lib/agent.ts +672 -0
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/claude.ts +918 -0
- package/lib/agents/codex-token-usage.fixture.json +130 -0
- package/lib/agents/codex.ts +1392 -0
- package/lib/agents/cursor-period-usage.ts +253 -0
- package/lib/agents/cursor.ts +562 -0
- package/lib/agents/dispatch.ts +78 -0
- package/lib/agents/durable-proc.ts +334 -0
- package/lib/agents/factory.ts +57 -0
- package/lib/agents/grok.ts +381 -0
- package/lib/agents/kimi.ts +329 -0
- package/lib/agents/machine-durable.ts +604 -0
- package/lib/agents/mock.ts +133 -0
- package/lib/agents/mode.ts +64 -0
- package/lib/agents/opencode.ts +508 -0
- package/lib/agents/output-source.ts +3 -0
- package/lib/agents/proc.ts +221 -0
- package/lib/agents/rate-limit.ts +337 -0
- package/lib/agents/registry.ts +182 -0
- package/lib/agents/session-state.ts +197 -0
- package/lib/agents/skill-index-preamble.fixture.ts +26 -0
- package/lib/agents/transport.ts +741 -0
- package/lib/agents/types.ts +254 -0
- package/lib/agents/uai.ts +106 -0
- package/lib/agents/usage.ts +44 -0
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +237 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/assignment-evidence.ts +94 -0
- package/lib/attachments.ts +48 -0
- package/lib/browser-testing.ts +1537 -0
- package/lib/capture-flow.ts +332 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2587 -0
- package/lib/cloud-credential-runtime.ts +1357 -0
- package/lib/cloud-state.ts +56 -0
- package/lib/codex-auth.ts +456 -0
- package/lib/command-db.ts +310 -0
- package/lib/compose-network-recovery.ts +283 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/container-runtime.ts +1453 -0
- package/lib/credential-cleanup-inventory.ts +490 -0
- package/lib/credential-consumers/engine-anchor.ts +1880 -0
- package/lib/credential-consumers/engines.ts +797 -0
- package/lib/credential-consumers/files.ts +934 -0
- package/lib/credential-consumers/github.ts +702 -0
- package/lib/credential-consumers/mcp.ts +389 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-consumers/task-secrets.ts +10 -0
- package/lib/credential-store-pickup.ts +149 -0
- package/lib/credential-store-scope.ts +112 -0
- package/lib/credential-store.ts +932 -0
- package/lib/credentials-activation-authority.ts +709 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +384 -0
- package/lib/custom-connection-runtime.ts +141 -0
- package/lib/db.ts +80 -0
- package/lib/disk-pressure.ts +42 -0
- package/lib/docker-exec.ts +156 -0
- package/lib/engine-accounts.ts +1970 -0
- package/lib/engine-connect.ts +321 -0
- package/lib/engine-credential-env.ts +52 -0
- package/lib/engine-login.ts +2933 -0
- package/lib/engines.ts +1342 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +246 -0
- package/lib/env-schema.ts +891 -0
- package/lib/env.ts +195 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +530 -0
- package/lib/git-identity.ts +715 -0
- package/lib/github-git-auth.ts +207 -0
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +2439 -0
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-env.ts +424 -0
- package/lib/host-logs.ts +965 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/launch-mode.ts +25 -0
- package/lib/lease.ts +239 -0
- package/lib/legacy-host.ts +769 -0
- package/lib/legacy-runtime-retirement.ts +424 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/machine-exec.ts +238 -0
- package/lib/machine-gateway-tunnel.ts +344 -0
- package/lib/machine-keys.ts +65 -0
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/machine-provider-aws.ts +923 -0
- package/lib/machine-provider-local.ts +338 -0
- package/lib/machine-provider.ts +90 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +57 -0
- package/lib/managed-runtime.ts +4296 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-capture.ts +865 -0
- package/lib/mcp-config-lock.ts +2 -0
- package/lib/mcp-connections.ts +959 -0
- package/lib/mcp-gateway.ts +2594 -0
- package/lib/obs.ts +512 -0
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +11409 -0
- package/lib/person-capture.ts +327 -0
- package/lib/preview-ports.ts +85 -0
- package/lib/preview-sidecar.ts +331 -0
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-loader.ts +242 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/release-feed.ts +61 -0
- package/lib/release-manifest.ts +710 -0
- package/lib/release-trust.ts +46 -0
- package/lib/repo-clone.ts +38 -0
- package/lib/routing/shadow.ts +261 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +279 -0
- package/lib/runtime-archive.ts +1216 -0
- package/lib/runtime-authority.ts +166 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +1033 -0
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/address.ts +95 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +57 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +150 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +605 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/secrets.ts +71 -0
- package/lib/shared-files-s3.ts +976 -0
- package/lib/shared-files.ts +156 -0
- package/lib/sidecar-assign.ts +49 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +178 -0
- package/lib/sidecar.ts +69 -0
- package/lib/signin-box-credentials.ts +465 -0
- package/lib/signin-box.ts +114 -0
- package/lib/skill-index-example.fixture.ts +56 -0
- package/lib/skill-layout.ts +417 -0
- package/lib/skills.ts +489 -0
- package/lib/ssh.ts +401 -0
- package/lib/standard-image-cleanup.ts +117 -0
- package/lib/standard-image-pin.ts +207 -0
- package/lib/standard-image.ts +1697 -0
- package/lib/stopped-task-status-queue.ts +120 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-credential-cleanup.ts +752 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +91 -0
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +249 -0
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +860 -0
- package/lib/task-environment/docker.ts +1344 -0
- package/lib/task-environment/index.ts +466 -0
- package/lib/task-environment/legacy-adoption.ts +467 -0
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +1227 -0
- package/lib/task-environment/machine.ts +974 -0
- package/lib/task-environment/registry.ts +59 -0
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +566 -0
- package/lib/task-environment/types.ts +642 -0
- package/lib/task-environment/workspace-files.ts +685 -0
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-files-machine-retirement-outbox.ts +592 -0
- package/lib/task-files-machine-retirement.ts +116 -0
- package/lib/task-files-readiness.ts +127 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +47 -0
- package/lib/task-gateway-state.ts +512 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +682 -0
- package/lib/task-machine-credentials-activation.ts +95 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/task-person-credentials.ts +76 -0
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1062 -0
- package/lib/task-secret-staging.ts +16 -0
- package/lib/task-status.ts +46 -0
- package/lib/task-token.ts +46 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +105 -0
- package/lib/token-broker-local.ts +392 -0
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +525 -0
- package/lib/token-broker.ts +218 -0
- package/lib/token-lanes/openai-oauth.ts +123 -0
- package/lib/token-lanes/registry.ts +45 -0
- package/lib/token-lanes/types.ts +163 -0
- package/lib/token-lanes/xai-oauth.ts +165 -0
- package/lib/transcript.ts +128 -0
- package/lib/tunnel-registry.ts +220 -0
- package/lib/tunnel-runtime.ts +235 -0
- package/lib/ulid.ts +16 -0
- package/lib/user-bus-env.ts +116 -0
- package/lib/user-credentials-store.ts +435 -0
- package/lib/vnode-pressure.ts +142 -0
- package/package.json +115 -8
- package/runner/runner.mjs +442 -0
- package/scripts/agent/_common.sh +633 -0
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/moondeck-cli.sh +5 -0
- package/scripts/agent/task-down.sh +291 -0
- package/scripts/agent/task-status.sh +70 -0
- package/scripts/agent/task-up.sh +3761 -0
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/scripts/install/darwin.ts +1048 -0
- package/scripts/install/linux.ts +958 -0
- package/scripts/install/types.ts +78 -0
- package/scripts/install/util.ts +254 -0
- package/scripts/install/win.ts +143 -0
- package/src/activation-respawn.ts +155 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/capture-wire.ts +102 -0
- package/src/cli.ts +2583 -0
- package/src/cloud-credentials.ts +2144 -0
- package/src/cloud-origins.ts +23 -0
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +237 -0
- package/src/engine-account-paths.ts +11 -0
- package/src/engine-models.ts +39 -0
- package/src/engine-usage.ts +104 -0
- package/src/event-outbox.ts +185 -0
- package/src/index.ts +2899 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6349 -0
- package/src/never-proxied-domains.ts +31 -0
- package/src/outbound-frame.ts +31 -0
- package/src/paths.ts +81 -0
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +396 -0
- package/src/project-env-wording.ts +53 -0
- package/src/protocol.ts +5545 -0
- package/src/runtime-bootstrap.ts +165 -0
- package/src/station-identity.ts +96 -0
- package/src/task-env-names.ts +28 -0
- package/src/task-secrets.ts +75 -0
- package/src/tunnel-dial.ts +102 -0
- package/src/ui/server.ts +852 -0
- package/src/ui/types.ts +173 -0
- package/src/workspace-path-policy.ts +41 -0
- package/ui/app.js +1102 -0
- package/ui/index.html +90 -0
- package/ui/style.css +685 -0
- package/ui/uai-favicon.svg +21 -0
- package/ui/uai-logo-black.svg +9 -0
- package/ui/uai-wheel.svg +9 -0
- package/bin/moondeck-station.mjs +0 -3
package/lib/git-diff.ts
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `git diff` unified output into structured per-file patches.
|
|
3
|
+
*
|
|
4
|
+
* Input is whatever `git diff <merge-base>` (committed + uncommitted) plus
|
|
5
|
+
* appended `--no-index` new-file patches for untracked files produce —
|
|
6
|
+
* multi-file, with headers, mode/rename/binary annotations, and `@@` hunks.
|
|
7
|
+
* The output is a list of `DiffFile`s the UI can render per-file with
|
|
8
|
+
* collapse/expand and syntax-highlighted hunks.
|
|
9
|
+
*
|
|
10
|
+
* Hand-rolled rather than pulled in as a dep — the unified-diff grammar
|
|
11
|
+
* is small and the chat-side Markdown renderer already sets the
|
|
12
|
+
* dependency-free precedent (see components/markdown.tsx).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { RUNTIME_AUTHORITY_ENV } from "./runtime-authority";
|
|
16
|
+
import type {
|
|
17
|
+
TaskEnvironmentExecRequest,
|
|
18
|
+
TaskEnvironmentExecResult,
|
|
19
|
+
} from "./task-environment/types";
|
|
20
|
+
|
|
21
|
+
export type DiffStatus =
|
|
22
|
+
| "added"
|
|
23
|
+
| "deleted"
|
|
24
|
+
| "modified"
|
|
25
|
+
| "renamed"
|
|
26
|
+
| "copied"
|
|
27
|
+
| "binary";
|
|
28
|
+
|
|
29
|
+
export interface DiffLine {
|
|
30
|
+
/** ' ' for context, '+' for added, '-' for removed. */
|
|
31
|
+
kind: "ctx" | "add" | "del";
|
|
32
|
+
/** Line content without the leading marker. */
|
|
33
|
+
content: string;
|
|
34
|
+
/** Line number in the old file (1-based) for ctx/del lines. */
|
|
35
|
+
oldNo?: number;
|
|
36
|
+
/** Line number in the new file (1-based) for ctx/add lines. */
|
|
37
|
+
newNo?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DiffHunk {
|
|
41
|
+
oldStart: number;
|
|
42
|
+
oldLines: number;
|
|
43
|
+
newStart: number;
|
|
44
|
+
newLines: number;
|
|
45
|
+
/** Raw `@@ ... @@` header text, useful for the section label git
|
|
46
|
+
* sometimes includes after the second `@@`. */
|
|
47
|
+
header: string;
|
|
48
|
+
lines: DiffLine[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DiffFile {
|
|
52
|
+
path: string;
|
|
53
|
+
/** Set on rename / copy — the source path before the change. */
|
|
54
|
+
oldPath?: string;
|
|
55
|
+
status: DiffStatus;
|
|
56
|
+
/** Best-effort language hint based on the file extension. */
|
|
57
|
+
language?: string;
|
|
58
|
+
/** A `Binary files … differ` patch has no hunks. */
|
|
59
|
+
binary: boolean;
|
|
60
|
+
hunks: DiffHunk[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** How every patch is rendered: no colour, no external diff driver, the
|
|
64
|
+
* usual a/ b/ prefixes, paths as they are. */
|
|
65
|
+
const GIT_DIFF_ARGS = "-c core.quotepath=false diff --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/";
|
|
66
|
+
|
|
67
|
+
const NON_INTERACTIVE_GIT_ENV = {
|
|
68
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
69
|
+
GCM_INTERACTIVE: "never",
|
|
70
|
+
GIT_SSH_COMMAND: "ssh -o BatchMode=yes",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** A diff's bound on what each of a project's producers may give: its
|
|
74
|
+
* tracked patches, and its untracked ones. Past it, the project's diff is
|
|
75
|
+
* an error, never a partial patch. */
|
|
76
|
+
export const TASK_DIFF_PATCH_BYTES = 8 * 1024 * 1024;
|
|
77
|
+
/** A diff's bound on its whole output, however many projects it has: the
|
|
78
|
+
* projects past it are errors. */
|
|
79
|
+
export const TASK_DIFF_OUTPUT_BYTES = 48 * 1024 * 1024;
|
|
80
|
+
/** A diff's bound on the lines it renders, every project of it: each line
|
|
81
|
+
* becomes an object, so a valid patch of millions of tiny lines within the
|
|
82
|
+
* byte bounds would otherwise cost the runner hundreds of MiB to parse. A
|
|
83
|
+
* project that would pass what is left of it is an error, unparsed. */
|
|
84
|
+
export const TASK_DIFF_MAX_LINES = 100_000;
|
|
85
|
+
/** A diff's bound on its result as the runner sends it: JSON-escaped, a
|
|
86
|
+
* patch's control bytes take six bytes each, so patches within the byte and
|
|
87
|
+
* line bounds could still serialize past the bridge's frame limit (100 MiB)
|
|
88
|
+
* and get the runner's connection closed. A project that would pass what is
|
|
89
|
+
* left of it is an error, unparsed. */
|
|
90
|
+
export const TASK_DIFF_RESULT_BYTES = 48 * 1024 * 1024;
|
|
91
|
+
/** What each rendered line adds to the result beyond its own text: its
|
|
92
|
+
* object, keys and line numbers. */
|
|
93
|
+
export const TASK_DIFF_LINE_RESULT_BYTES = 96;
|
|
94
|
+
/** What each project's entry may take in the result besides its files: its
|
|
95
|
+
* id, name, path, base and error, each at its bound and JSON-escaped at
|
|
96
|
+
* worst, and their keys. Reserved from the result bound up front. */
|
|
97
|
+
export const TASK_DIFF_REPO_RESULT_BYTES = 16 * 1024;
|
|
98
|
+
/** A project's reported error, and its base, at most: both are the task's
|
|
99
|
+
* own (its Git's stderr, its remote's default branch). */
|
|
100
|
+
export const TASK_DIFF_ERROR_CHARS = 1024;
|
|
101
|
+
export const TASK_DIFF_BASE_CHARS = 256;
|
|
102
|
+
/** How long a diff may take, every project of it: it is killed inside its
|
|
103
|
+
* runtime past this, everything it started with it. */
|
|
104
|
+
export const TASK_DIFF_TIMEOUT_MS = 300_000;
|
|
105
|
+
/** Cap how many untracked files are rendered as new-file patches, so a
|
|
106
|
+
* pathological untracked directory that slips past .gitignore cannot
|
|
107
|
+
* turn one diff into thousands of patches. */
|
|
108
|
+
export const MAX_UNTRACKED = 200;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* `bounded CMD...` (POSIX sh): runs CMD with its output in `$tmp/out` (at
|
|
112
|
+
* most `$limit` bytes), its errors in `$tmp/err` (at most 4 KiB) and its
|
|
113
|
+
* status in `$tmp/status`. Each stream goes through a pipe whose reader
|
|
114
|
+
* stops at its bound, so a writer past it is stopped (SIGPIPE) and nothing
|
|
115
|
+
* past it is held, in memory or on disk.
|
|
116
|
+
*/
|
|
117
|
+
export const TASK_DIFF_BOUNDED = [
|
|
118
|
+
"bounded() {",
|
|
119
|
+
' echo 1 >"$tmp/status"',
|
|
120
|
+
' { { "$@"; echo "$?" >"$tmp/status"; } 2>&1 1>&3 3>&- | head -c 4096 >"$tmp/err"; } 3>&1 | head -c "$limit" >"$tmp/out"',
|
|
121
|
+
"}",
|
|
122
|
+
].join("\n");
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A task's whole diff, as one POSIX sh run in the task's environment (its
|
|
126
|
+
* runtime while it runs, a stopped task's sandbox otherwise; never the
|
|
127
|
+
* host, #cb3j0z): `$1` the bound on each producer's bytes, `$2` the
|
|
128
|
+
* untracked bound, `$3` the bound on the whole output, then each project's
|
|
129
|
+
* directory below the working directory. For each project it resolves the
|
|
130
|
+
* base (the remote's default branch, `origin/HEAD`, else `origin/main`:
|
|
131
|
+
* never the worktree's upstream, which flips to the pushed task branch and
|
|
132
|
+
* would hide all committed work), diffs the merge-base against the working
|
|
133
|
+
* tree (committed + staged + unstaged), and renders each untracked file
|
|
134
|
+
* (honouring .gitignore) as a new-file patch.
|
|
135
|
+
*
|
|
136
|
+
* Whatever runs for it, the task's own config drivers included, is bounded
|
|
137
|
+
* where it writes: its output at one byte past the bound and its errors at
|
|
138
|
+
* 4 KiB, each through a pipe that stops the writer once full, so nothing it
|
|
139
|
+
* produces is held beyond that, in memory or on disk.
|
|
140
|
+
*
|
|
141
|
+
* Every value is a line `<kind> <base64>`, so any bytes survive whatever
|
|
142
|
+
* carries them: `P` a project, then either `E` an error, or `B` its base,
|
|
143
|
+
* `D` its tracked patches and `U` its untracked ones (or `E`). The whole
|
|
144
|
+
* output stops at its bound.
|
|
145
|
+
*/
|
|
146
|
+
export const TASK_DIFF_SCRIPT = [
|
|
147
|
+
"set -u",
|
|
148
|
+
'each=$1; most=$2; cap=$3; shift 3',
|
|
149
|
+
'limit=$((each + 1))',
|
|
150
|
+
'tmp=$(mktemp -d "${TMPDIR:-/tmp}/uai-task-diff.XXXXXX") || exit 70',
|
|
151
|
+
"trap 'rm -rf \"$tmp\"' EXIT",
|
|
152
|
+
"root=$(pwd)",
|
|
153
|
+
'frame() { printf "%s " "$1"; base64 -w0 <"$2"; printf "\\n"; }',
|
|
154
|
+
'value() { printf "%s" "$2" >"$tmp/value"; frame "$1" "$tmp/value"; }',
|
|
155
|
+
TASK_DIFF_BOUNDED,
|
|
156
|
+
'over() { [ "$(wc -c <"$tmp/out")" -gt "$each" ]; }',
|
|
157
|
+
"untracked() {",
|
|
158
|
+
' git ls-files --others --exclude-standard -z 2>/dev/null | head -z -n "$most" \\',
|
|
159
|
+
` | xargs -0 -r -n 1 sh -c 'git ${GIT_DIFF_ARGS} --no-index -- /dev/null "$1"; [ $? -le 1 ] || exit 255' untracked`,
|
|
160
|
+
"}",
|
|
161
|
+
"projects() {",
|
|
162
|
+
' for slug in "$@"; do',
|
|
163
|
+
' value P "$slug"',
|
|
164
|
+
' if ! cd -- "$root/$slug" 2>/dev/null; then value E "not a project directory: $slug"; continue; fi',
|
|
165
|
+
" base=$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null) || base=",
|
|
166
|
+
' [ -n "$base" ] || base=origin/main',
|
|
167
|
+
' value B "$base"',
|
|
168
|
+
' merge=$(git merge-base "$base" HEAD 2>/dev/null) || merge=',
|
|
169
|
+
' [ -n "$merge" ] || merge=$base',
|
|
170
|
+
` bounded git ${GIT_DIFF_ARGS} "$merge"`,
|
|
171
|
+
' if over; then value E "the diff exceeded $each bytes"',
|
|
172
|
+
' elif [ "$(cat "$tmp/status")" != 0 ]; then frame E "$tmp/err"',
|
|
173
|
+
" else",
|
|
174
|
+
' frame D "$tmp/out"',
|
|
175
|
+
" bounded untracked",
|
|
176
|
+
' if over; then value E "the untracked files exceeded $each bytes"',
|
|
177
|
+
' elif [ "$(cat "$tmp/status")" != 0 ]; then frame E "$tmp/err"',
|
|
178
|
+
' else frame U "$tmp/out"; fi',
|
|
179
|
+
" fi",
|
|
180
|
+
' cd -- "$root"',
|
|
181
|
+
" done",
|
|
182
|
+
"}",
|
|
183
|
+
'projects "$@" | head -c "$cap"',
|
|
184
|
+
].join("\n");
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The one exec of a task's diff of `slugs`, below `workspacePath`: killed
|
|
188
|
+
* inside the runtime at its deadline, the shell and everything it started
|
|
189
|
+
* (`timeout` signals their whole group), however the client that started it
|
|
190
|
+
* ended.
|
|
191
|
+
*/
|
|
192
|
+
export function taskDiffRequest(
|
|
193
|
+
workspacePath: string,
|
|
194
|
+
slugs: readonly string[],
|
|
195
|
+
): TaskEnvironmentExecRequest {
|
|
196
|
+
return {
|
|
197
|
+
projectEnvProfile: "maintenance",
|
|
198
|
+
argv: [
|
|
199
|
+
"/usr/bin/timeout",
|
|
200
|
+
"--signal=KILL",
|
|
201
|
+
String(TASK_DIFF_TIMEOUT_MS / 1000),
|
|
202
|
+
"/bin/sh",
|
|
203
|
+
"-c",
|
|
204
|
+
TASK_DIFF_SCRIPT,
|
|
205
|
+
"uai-task-diff",
|
|
206
|
+
String(TASK_DIFF_PATCH_BYTES),
|
|
207
|
+
String(MAX_UNTRACKED),
|
|
208
|
+
String(TASK_DIFF_OUTPUT_BYTES),
|
|
209
|
+
...slugs,
|
|
210
|
+
],
|
|
211
|
+
cwd: workspacePath,
|
|
212
|
+
env: { ...RUNTIME_AUTHORITY_ENV, ...NON_INTERACTIVE_GIT_ENV },
|
|
213
|
+
// The client's own bound, past the runtime's.
|
|
214
|
+
timeoutMs: TASK_DIFF_TIMEOUT_MS + 15_000,
|
|
215
|
+
// The script stops at its bound; past it, the exec itself is cut.
|
|
216
|
+
maxOutputBytes: TASK_DIFF_OUTPUT_BYTES + 1024 * 1024,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The bytes `JSON.stringify(text)` takes, counted without making it: two
|
|
222
|
+
* for each quote or backslash and for each control character with a short
|
|
223
|
+
* escape, six for any other control character or lone surrogate, and each
|
|
224
|
+
* other character's UTF-8 width, plus the quotes around it.
|
|
225
|
+
*/
|
|
226
|
+
export function jsonEscapedBytes(text: string): number {
|
|
227
|
+
let bytes = 2;
|
|
228
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
229
|
+
const code = text.charCodeAt(index);
|
|
230
|
+
if (code === 0x22 || code === 0x5c) bytes += 2;
|
|
231
|
+
else if (code < 0x20) {
|
|
232
|
+
bytes += code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6;
|
|
233
|
+
} else if (code < 0x80) bytes += 1;
|
|
234
|
+
else if (code < 0x800) bytes += 2;
|
|
235
|
+
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length && (text.charCodeAt(index + 1) & 0xfc00) === 0xdc00) {
|
|
236
|
+
bytes += 4;
|
|
237
|
+
index += 1;
|
|
238
|
+
} else if (code >= 0xd800 && code <= 0xdfff) bytes += 6;
|
|
239
|
+
else bytes += 3;
|
|
240
|
+
}
|
|
241
|
+
return bytes;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** How many lines `text` holds, counted without parsing it (the last one
|
|
245
|
+
* counts without its newline). */
|
|
246
|
+
export function countDiffLines(text: string): number {
|
|
247
|
+
let lines = 0;
|
|
248
|
+
let at = text.indexOf("\n");
|
|
249
|
+
while (at !== -1) {
|
|
250
|
+
lines += 1;
|
|
251
|
+
at = text.indexOf("\n", at + 1);
|
|
252
|
+
}
|
|
253
|
+
return text.length > 0 && !text.endsWith("\n") ? lines + 1 : lines;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** One project of a diff's output. */
|
|
257
|
+
export interface TaskDiffProjectOutput {
|
|
258
|
+
slug: string;
|
|
259
|
+
base?: string;
|
|
260
|
+
/** Its tracked patches, then its untracked ones. */
|
|
261
|
+
patches?: string;
|
|
262
|
+
tracked?: boolean;
|
|
263
|
+
untracked?: boolean;
|
|
264
|
+
error?: string;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The projects of a diff's output (`TASK_DIFF_SCRIPT`), in order. Only whole
|
|
269
|
+
* lines count: a line the bound cut short is dropped, and its project is
|
|
270
|
+
* left without the value.
|
|
271
|
+
*/
|
|
272
|
+
export function parseTaskDiffOutput(output: string): TaskDiffProjectOutput[] {
|
|
273
|
+
const projects: TaskDiffProjectOutput[] = [];
|
|
274
|
+
const lines = output.split("\n");
|
|
275
|
+
// The last element follows the last newline: whole only when empty.
|
|
276
|
+
lines.pop();
|
|
277
|
+
for (const line of lines) {
|
|
278
|
+
if (line.length < 2 || line[1] !== " " || !/^[A-Za-z0-9+/]*={0,2}$/.test(line.slice(2))) continue;
|
|
279
|
+
const value = Buffer.from(line.slice(2), "base64").toString("utf8");
|
|
280
|
+
const kind = line[0];
|
|
281
|
+
if (kind === "P") {
|
|
282
|
+
projects.push({ slug: value });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const project = projects.at(-1);
|
|
286
|
+
if (!project) continue;
|
|
287
|
+
if (kind === "B") project.base = value.slice(0, TASK_DIFF_BASE_CHARS);
|
|
288
|
+
else if (kind === "E") project.error = value.trim().slice(0, TASK_DIFF_ERROR_CHARS) || "the diff failed";
|
|
289
|
+
else if (kind === "D") {
|
|
290
|
+
project.patches = value;
|
|
291
|
+
project.tracked = true;
|
|
292
|
+
} else if (kind === "U") {
|
|
293
|
+
if (project.patches && value.length > 0 && !project.patches.endsWith("\n")) project.patches += "\n";
|
|
294
|
+
project.patches = (project.patches ?? "") + value;
|
|
295
|
+
project.untracked = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return projects;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Why a diff's exec gave nothing to parse: no exit, a failed one, or an
|
|
302
|
+
* output past its bound. Null when it can be parsed. */
|
|
303
|
+
export function taskDiffExecFailure(result: TaskEnvironmentExecResult): string | null {
|
|
304
|
+
if (result.stdoutTruncated || result.stderrTruncated) return "the diff exceeded its output bound";
|
|
305
|
+
if (result.exitCode === 0) return null;
|
|
306
|
+
const stderr = Buffer.from(result.stderr).toString("utf8").trim().slice(0, TASK_DIFF_ERROR_CHARS - 64);
|
|
307
|
+
const outcome =
|
|
308
|
+
result.exitCode === null
|
|
309
|
+
? result.signal
|
|
310
|
+
? `was terminated by ${result.signal}`
|
|
311
|
+
: "did not start"
|
|
312
|
+
: `exited ${result.exitCode}`;
|
|
313
|
+
return `the task diff ${outcome}${stderr ? `: ${stderr}` : ""}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Parse the output of `git diff` into structured file patches.
|
|
318
|
+
*/
|
|
319
|
+
export function parseGitDiff(text: string): DiffFile[] {
|
|
320
|
+
if (!text) return [];
|
|
321
|
+
const lines = text.split("\n");
|
|
322
|
+
const files: DiffFile[] = [];
|
|
323
|
+
let i = 0;
|
|
324
|
+
|
|
325
|
+
while (i < lines.length) {
|
|
326
|
+
const line = lines[i] ?? "";
|
|
327
|
+
if (!line.startsWith("diff --git ")) {
|
|
328
|
+
i += 1;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
// Each file section starts with `diff --git a/<path> b/<path>` and
|
|
332
|
+
// runs to the next `diff --git` line (or EOF).
|
|
333
|
+
const header = parseDiffGitHeader(line);
|
|
334
|
+
let path = header.bPath ?? header.aPath ?? "";
|
|
335
|
+
let oldPath: string | undefined;
|
|
336
|
+
let status: DiffStatus = "modified";
|
|
337
|
+
let binary = false;
|
|
338
|
+
const hunks: DiffHunk[] = [];
|
|
339
|
+
|
|
340
|
+
i += 1;
|
|
341
|
+
while (i < lines.length && !(lines[i] ?? "").startsWith("diff --git ")) {
|
|
342
|
+
const ln = lines[i] ?? "";
|
|
343
|
+
if (ln.startsWith("new file mode")) status = "added";
|
|
344
|
+
else if (ln.startsWith("deleted file mode")) status = "deleted";
|
|
345
|
+
else if (ln.startsWith("rename from ")) {
|
|
346
|
+
status = "renamed";
|
|
347
|
+
oldPath = ln.slice("rename from ".length);
|
|
348
|
+
} else if (ln.startsWith("rename to ")) {
|
|
349
|
+
path = ln.slice("rename to ".length);
|
|
350
|
+
} else if (ln.startsWith("copy from ")) {
|
|
351
|
+
status = "copied";
|
|
352
|
+
oldPath = ln.slice("copy from ".length);
|
|
353
|
+
} else if (ln.startsWith("copy to ")) {
|
|
354
|
+
path = ln.slice("copy to ".length);
|
|
355
|
+
} else if (ln.startsWith("Binary files ")) {
|
|
356
|
+
binary = true;
|
|
357
|
+
status = status === "modified" ? "binary" : status;
|
|
358
|
+
} else if (ln.startsWith("--- ")) {
|
|
359
|
+
const p = stripPathPrefix(ln.slice(4));
|
|
360
|
+
if (p !== "/dev/null" && status === "modified") oldPath = oldPath ?? p;
|
|
361
|
+
if (p === "/dev/null") status = "added";
|
|
362
|
+
} else if (ln.startsWith("+++ ")) {
|
|
363
|
+
const p = stripPathPrefix(ln.slice(4));
|
|
364
|
+
if (p !== "/dev/null") path = p;
|
|
365
|
+
else status = "deleted";
|
|
366
|
+
} else if (ln.startsWith("@@")) {
|
|
367
|
+
const { hunk, consumed } = parseHunk(lines, i);
|
|
368
|
+
if (hunk) hunks.push(hunk);
|
|
369
|
+
i += consumed;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
i += 1;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
files.push({
|
|
376
|
+
path,
|
|
377
|
+
oldPath: oldPath && oldPath !== path ? oldPath : undefined,
|
|
378
|
+
status,
|
|
379
|
+
language: languageFor(path),
|
|
380
|
+
binary,
|
|
381
|
+
hunks,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return files;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function parseDiffGitHeader(line: string): {
|
|
389
|
+
aPath?: string;
|
|
390
|
+
bPath?: string;
|
|
391
|
+
} {
|
|
392
|
+
// `diff --git a/foo b/foo` — paths may contain spaces if quoted.
|
|
393
|
+
// Quoted form is `diff --git "a/path with space" "b/path with space"`.
|
|
394
|
+
// For simplicity (uai paths are well-behaved), greedy match on the
|
|
395
|
+
// common forms.
|
|
396
|
+
const m = line.match(
|
|
397
|
+
/^diff --git (?:"a\/(.*?)"|a\/(\S+)) (?:"b\/(.*?)"|b\/(\S+))$/,
|
|
398
|
+
);
|
|
399
|
+
if (!m) return {};
|
|
400
|
+
const a = m[1] ?? m[2];
|
|
401
|
+
const b = m[3] ?? m[4];
|
|
402
|
+
return { aPath: a, bPath: b };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function stripPathPrefix(s: string): string {
|
|
406
|
+
// `a/foo`, `b/foo`, or `/dev/null`. Quoted form: `"a/foo"`.
|
|
407
|
+
const t = s.trim().replace(/^"(.*)"$/, "$1");
|
|
408
|
+
if (t === "/dev/null") return t;
|
|
409
|
+
if (t.startsWith("a/") || t.startsWith("b/")) return t.slice(2);
|
|
410
|
+
return t;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function parseHunk(
|
|
414
|
+
lines: string[],
|
|
415
|
+
start: number,
|
|
416
|
+
): { hunk: DiffHunk | null; consumed: number } {
|
|
417
|
+
const headerLine = lines[start] ?? "";
|
|
418
|
+
// `@@ -<oldStart>,<oldLines> +<newStart>,<newLines> @@ optional label`
|
|
419
|
+
const m = headerLine.match(
|
|
420
|
+
/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/,
|
|
421
|
+
);
|
|
422
|
+
if (!m) return { hunk: null, consumed: 1 };
|
|
423
|
+
const oldStart = Number(m[1]);
|
|
424
|
+
const oldLines = m[2] === undefined ? 1 : Number(m[2]);
|
|
425
|
+
const newStart = Number(m[3]);
|
|
426
|
+
const newLines = m[4] === undefined ? 1 : Number(m[4]);
|
|
427
|
+
const header = m[5] ?? "";
|
|
428
|
+
|
|
429
|
+
const out: DiffLine[] = [];
|
|
430
|
+
let oldNo = oldStart;
|
|
431
|
+
let newNo = newStart;
|
|
432
|
+
let i = start + 1;
|
|
433
|
+
let oldSeen = 0;
|
|
434
|
+
let newSeen = 0;
|
|
435
|
+
|
|
436
|
+
while (i < lines.length) {
|
|
437
|
+
const ln = lines[i] ?? "";
|
|
438
|
+
// End conditions: next hunk in the same file, next file, or EOF.
|
|
439
|
+
if (ln.startsWith("@@") || ln.startsWith("diff --git ")) break;
|
|
440
|
+
// `` markers — annotate the previous
|
|
441
|
+
// line by not advancing counters. Just skip.
|
|
442
|
+
if (ln.startsWith("\\ ")) {
|
|
443
|
+
i += 1;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const c = ln.charAt(0);
|
|
447
|
+
if (c === "+") {
|
|
448
|
+
out.push({ kind: "add", content: ln.slice(1), newNo });
|
|
449
|
+
newNo += 1;
|
|
450
|
+
newSeen += 1;
|
|
451
|
+
} else if (c === "-") {
|
|
452
|
+
out.push({ kind: "del", content: ln.slice(1), oldNo });
|
|
453
|
+
oldNo += 1;
|
|
454
|
+
oldSeen += 1;
|
|
455
|
+
} else if (c === " " || ln === "") {
|
|
456
|
+
// Context line. Git emits a space prefix for context; an empty
|
|
457
|
+
// line in unified diff is still a context line (an empty file
|
|
458
|
+
// line).
|
|
459
|
+
out.push({
|
|
460
|
+
kind: "ctx",
|
|
461
|
+
content: c === " " ? ln.slice(1) : "",
|
|
462
|
+
oldNo,
|
|
463
|
+
newNo,
|
|
464
|
+
});
|
|
465
|
+
oldNo += 1;
|
|
466
|
+
newNo += 1;
|
|
467
|
+
oldSeen += 1;
|
|
468
|
+
newSeen += 1;
|
|
469
|
+
} else {
|
|
470
|
+
// Anything else — probably the start of metadata for a fresh
|
|
471
|
+
// file we somehow didn't catch. Bail out so the outer loop can
|
|
472
|
+
// re-process it.
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
i += 1;
|
|
476
|
+
// Hunks are self-delimiting by their counts. Once we've consumed
|
|
477
|
+
// them, stop — anything further belongs to the next section.
|
|
478
|
+
if (oldSeen >= oldLines && newSeen >= newLines) break;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
hunk: {
|
|
483
|
+
oldStart,
|
|
484
|
+
oldLines,
|
|
485
|
+
newStart,
|
|
486
|
+
newLines,
|
|
487
|
+
header,
|
|
488
|
+
lines: out,
|
|
489
|
+
},
|
|
490
|
+
consumed: i - start,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const LANG_BY_EXT: Record<string, string> = {
|
|
495
|
+
ts: "typescript",
|
|
496
|
+
tsx: "typescript",
|
|
497
|
+
js: "javascript",
|
|
498
|
+
jsx: "javascript",
|
|
499
|
+
mjs: "javascript",
|
|
500
|
+
cjs: "javascript",
|
|
501
|
+
json: "json",
|
|
502
|
+
md: "markdown",
|
|
503
|
+
mdx: "markdown",
|
|
504
|
+
css: "css",
|
|
505
|
+
scss: "scss",
|
|
506
|
+
html: "html",
|
|
507
|
+
yml: "yaml",
|
|
508
|
+
yaml: "yaml",
|
|
509
|
+
toml: "toml",
|
|
510
|
+
py: "python",
|
|
511
|
+
rb: "ruby",
|
|
512
|
+
go: "go",
|
|
513
|
+
rs: "rust",
|
|
514
|
+
java: "java",
|
|
515
|
+
c: "c",
|
|
516
|
+
h: "c",
|
|
517
|
+
cpp: "cpp",
|
|
518
|
+
hpp: "cpp",
|
|
519
|
+
sh: "bash",
|
|
520
|
+
bash: "bash",
|
|
521
|
+
sql: "sql",
|
|
522
|
+
swift: "swift",
|
|
523
|
+
kt: "kotlin",
|
|
524
|
+
php: "php",
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
function languageFor(path: string): string | undefined {
|
|
528
|
+
const ext = path.includes(".") ? path.split(".").pop()!.toLowerCase() : "";
|
|
529
|
+
return LANG_BY_EXT[ext];
|
|
530
|
+
}
|