@sayknow-cli/coding-agent 0.3.7 → 0.3.9
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/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +119 -1
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +30 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +8 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +12 -3
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon.d.ts +45 -3
- package/dist/types/notifications/telegram-reference.d.ts +7 -5
- package/dist/types/notifications/threaded-render.d.ts +14 -0
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +61 -6
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
- package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/skc-runtime/state-writer.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +8 -8
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +61 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +103 -17
- package/src/cli/telegram-cli.ts +1 -1
- package/src/cli/update-cli.ts +148 -33
- package/src/cli.ts +29 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +3 -3
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-profiles.ts +31 -2
- package/src/config/settings-schema.ts +76 -1
- package/src/config/settings.ts +22 -0
- package/src/defaults/skc/skills/team/SKILL.md +17 -14
- package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/skc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +183 -219
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +124 -4
- package/src/internal-urls/docs-index.generated.ts +17 -8
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +95 -43
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/components/welcome.ts +181 -44
- package/src/modes/controllers/command-controller.ts +40 -4
- package/src/modes/controllers/event-controller.ts +77 -49
- package/src/modes/controllers/extension-ui-controller.ts +12 -11
- package/src/modes/controllers/input-controller.ts +192 -35
- package/src/modes/controllers/selector-controller.ts +168 -24
- package/src/modes/interactive-mode.ts +56 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/rpc/rpc-mode.ts +32 -12
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +20 -7
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +52 -5
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +144 -18
- package/src/notifications/lifecycle-commands.ts +74 -23
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon-control.ts +5 -3
- package/src/notifications/telegram-daemon.ts +606 -98
- package/src/notifications/telegram-reference.ts +17 -16
- package/src/notifications/threaded-render.ts +45 -2
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +29 -17
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/computer.md +2 -2
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +6 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +88 -36
- package/src/session/agent-session.ts +1013 -141
- package/src/session/agent-storage.ts +4 -0
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +103 -4
- package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/skc-runtime/deep-interview-recorder.ts +62 -8
- package/src/skc-runtime/deep-interview-state.ts +44 -2
- package/src/skc-runtime/session-state-sidecar.ts +263 -20
- package/src/skc-runtime/state-runtime.ts +12 -2
- package/src/skc-runtime/state-writer.ts +15 -0
- package/src/skc-runtime/tmux-common.ts +1 -0
- package/src/skc-runtime/ultragoal-guard.ts +55 -66
- package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/skc-runtime/ultragoal-runtime.ts +1672 -159
- package/src/skc-runtime/workflow-manifest.generated.json +113 -2
- package/src/skc-runtime/workflow-manifest.ts +74 -1
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +296 -72
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +132 -24
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +13 -6
- package/src/task/output-manager.ts +20 -8
- package/src/task/receipt.ts +26 -7
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/computer.ts +246 -43
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/tools/telegram-send.ts +6 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/utils/pasted-image-path.ts +170 -0
- package/src/web/scrapers/youtube.ts +24 -12
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { SkcSessionContext } from "../skc-runtime/session-layout";
|
|
4
|
+
import { resolveSkcSessionForRead, SessionResolutionError } from "../skc-runtime/session-resolution";
|
|
5
|
+
import { computeTaskTokenMetrics, readTaskTokenLogs } from "../task/token-log";
|
|
6
|
+
import type { TaskTokenLog } from "../task/types";
|
|
7
|
+
|
|
8
|
+
const LIVE_RUNNER_SCHEMA_VERSION = 1;
|
|
9
|
+
const BINARY_ID = "skc";
|
|
10
|
+
|
|
11
|
+
function deterministicLog(
|
|
12
|
+
input: number,
|
|
13
|
+
output: number,
|
|
14
|
+
cacheRead: number,
|
|
15
|
+
cacheWrite: number,
|
|
16
|
+
totalTokens: number,
|
|
17
|
+
): TaskTokenLog {
|
|
18
|
+
return {
|
|
19
|
+
subagentId: "root",
|
|
20
|
+
agent: "main",
|
|
21
|
+
turn: 1,
|
|
22
|
+
at: "2026-01-01T00:00:00.000Z",
|
|
23
|
+
input,
|
|
24
|
+
output,
|
|
25
|
+
cacheRead,
|
|
26
|
+
cacheWrite,
|
|
27
|
+
totalTokens,
|
|
28
|
+
model: "fixture-model",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LiveRunReportShape {
|
|
33
|
+
schemaVersion: 1;
|
|
34
|
+
binaryId: string;
|
|
35
|
+
fixtureId: string;
|
|
36
|
+
totals: {
|
|
37
|
+
turns: number;
|
|
38
|
+
inputTokens: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
cacheReadTokens: number;
|
|
41
|
+
cacheWriteTokens: number;
|
|
42
|
+
totalTokens: number;
|
|
43
|
+
};
|
|
44
|
+
cacheHitRate: number | null;
|
|
45
|
+
receiptArtifactRatio: number | null;
|
|
46
|
+
spawnDecisions: number | null;
|
|
47
|
+
roi: number | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DETERMINISTIC_FIXTURES: Record<string, readonly TaskTokenLog[]> = {
|
|
51
|
+
"fixed-fixture": [
|
|
52
|
+
{
|
|
53
|
+
subagentId: "root",
|
|
54
|
+
agent: "main",
|
|
55
|
+
turn: 1,
|
|
56
|
+
at: "2026-01-01T00:00:00.000Z",
|
|
57
|
+
input: 100,
|
|
58
|
+
output: 20,
|
|
59
|
+
cacheRead: 40,
|
|
60
|
+
cacheWrite: 10,
|
|
61
|
+
totalTokens: 170,
|
|
62
|
+
model: "fixture-model",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
subagentId: "1-executor",
|
|
66
|
+
agent: "executor",
|
|
67
|
+
turn: 1,
|
|
68
|
+
at: "2026-01-01T00:00:01.000Z",
|
|
69
|
+
input: 60,
|
|
70
|
+
output: 15,
|
|
71
|
+
cacheRead: 20,
|
|
72
|
+
cacheWrite: 5,
|
|
73
|
+
totalTokens: 100,
|
|
74
|
+
model: "fixture-model",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
const DEFAULT_REDUCTION_FIXTURE_LOGS: Record<string, readonly TaskTokenLog[]> = {
|
|
79
|
+
"pr9.task-recursion.before": [deterministicLog(2400, 420, 600, 200, 3620)],
|
|
80
|
+
"pr9.task-recursion.after": [deterministicLog(1800, 360, 450, 150, 2760)],
|
|
81
|
+
"pr9.output-caps.before": [deterministicLog(22_000, 3_200, 0, 0, 25_200)],
|
|
82
|
+
"pr9.output-caps.after": [deterministicLog(18_000, 2_400, 0, 0, 20_400)],
|
|
83
|
+
"pr9.max-inline-result-bytes.before": [deterministicLog(48_000, 2_000, 0, 0, 50_000)],
|
|
84
|
+
"pr9.max-inline-result-bytes.after": [deterministicLog(12_000, 1_600, 0, 0, 13_600)],
|
|
85
|
+
"pr9.read-artifact-spill-threshold.before": [deterministicLog(96_000, 2_500, 0, 0, 98_500)],
|
|
86
|
+
"pr9.read-artifact-spill-threshold.after": [deterministicLog(18_000, 2_100, 0, 0, 20_100)],
|
|
87
|
+
"pr9.maintenance-pruning.before": [deterministicLog(40_000, 2_000, 18_000, 6_000, 66_000)],
|
|
88
|
+
"pr9.maintenance-pruning.after": [deterministicLog(30_000, 1_800, 10_000, 3_000, 44_800)],
|
|
89
|
+
"pr9.append-only-provider-expansion.before": [deterministicLog(28_000, 1_700, 8_000, 2_500, 40_200)],
|
|
90
|
+
"pr9.append-only-provider-expansion.after": [deterministicLog(20_000, 1_700, 14_000, 2_500, 38_200)],
|
|
91
|
+
"pr9.rpc-compact-deltas.before": [deterministicLog(14_000, 1_200, 6_000, 1_500, 22_700)],
|
|
92
|
+
"pr9.rpc-compact-deltas.after": [deterministicLog(10_000, 900, 6_200, 1_500, 18_600)],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export function buildFixtureReport(fixtureId: string, logs: readonly TaskTokenLog[]): LiveRunReportShape {
|
|
96
|
+
const metrics = computeTaskTokenMetrics(logs);
|
|
97
|
+
return {
|
|
98
|
+
schemaVersion: LIVE_RUNNER_SCHEMA_VERSION,
|
|
99
|
+
binaryId: BINARY_ID,
|
|
100
|
+
fixtureId,
|
|
101
|
+
totals: {
|
|
102
|
+
turns: metrics.turns,
|
|
103
|
+
inputTokens: metrics.inputTokens,
|
|
104
|
+
outputTokens: metrics.outputTokens,
|
|
105
|
+
cacheReadTokens: metrics.cacheReadTokens,
|
|
106
|
+
cacheWriteTokens: metrics.cacheWriteTokens,
|
|
107
|
+
totalTokens: metrics.totalTokens,
|
|
108
|
+
},
|
|
109
|
+
cacheHitRate: logs.length === 0 ? null : metrics.cacheHitRate,
|
|
110
|
+
receiptArtifactRatio: null,
|
|
111
|
+
spawnDecisions: null,
|
|
112
|
+
roi: null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function runFixtureReport(fixtureId: string): Promise<number> {
|
|
117
|
+
let resolved: ResolvedFixtureLogs;
|
|
118
|
+
try {
|
|
119
|
+
resolved = await resolveFixtureLogs(fixtureId);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// e.g. a corrupt token-log. Fail loudly rather than emitting an all-zero report.
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
`failed to build fixture report for ${fixtureId}: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
124
|
+
);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
if (resolved.kind === "unknown") {
|
|
128
|
+
// Neither a known deterministic fixture nor a resolvable SKC session. Emit a
|
|
129
|
+
// bounded error on stderr with a non-zero exit rather than a schema-valid
|
|
130
|
+
// all-zero report, which a before/after benchmark would misread as "0 tokens".
|
|
131
|
+
process.stderr.write(`unknown fixture id and no matching SKC session: ${fixtureId}\n`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
process.stdout.write(JSON.stringify(buildFixtureReport(fixtureId, resolved.logs)));
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type ResolvedFixtureLogs =
|
|
139
|
+
| { readonly kind: "logs"; readonly logs: readonly TaskTokenLog[] }
|
|
140
|
+
| { readonly kind: "unknown" };
|
|
141
|
+
|
|
142
|
+
async function resolveFixtureLogs(fixtureId: string): Promise<ResolvedFixtureLogs> {
|
|
143
|
+
const deterministic = DETERMINISTIC_FIXTURES[fixtureId] ?? DEFAULT_REDUCTION_FIXTURE_LOGS[fixtureId];
|
|
144
|
+
if (deterministic) return { kind: "logs", logs: deterministic };
|
|
145
|
+
let session: SkcSessionContext;
|
|
146
|
+
try {
|
|
147
|
+
session = await resolveSkcSessionForRead(process.cwd(), {
|
|
148
|
+
flagValue: fixtureId,
|
|
149
|
+
envSessionId: process.env.SKC_SESSION_ID,
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error instanceof SessionResolutionError) return { kind: "unknown" };
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
// resolveSkcSessionForRead accepts any explicit flagValue as a session id
|
|
156
|
+
// without checking the dir exists, so a typo would otherwise yield a
|
|
157
|
+
// schema-valid all-zero report. Require the session root to exist; a real
|
|
158
|
+
// session with no turns yet still reads as a legitimate empty log set.
|
|
159
|
+
if (!(await directoryExists(session.sessionRoot))) return { kind: "unknown" };
|
|
160
|
+
const logs = await readTaskTokenLogs(path.join(session.sessionRoot, "token-logs"));
|
|
161
|
+
return { kind: "logs", logs };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function directoryExists(dir: string): Promise<boolean> {
|
|
165
|
+
try {
|
|
166
|
+
const stat = await fs.stat(dir);
|
|
167
|
+
return stat.isDirectory();
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export const RECOMMENDED_MACOS_NOFILE_LIMIT = 4096;
|
|
4
|
+
|
|
5
|
+
export function parseNoFileLimit(text: string): number | undefined {
|
|
6
|
+
const trimmed = text.trim();
|
|
7
|
+
if (!trimmed || trimmed === "unlimited") return undefined;
|
|
8
|
+
const value = Number(trimmed);
|
|
9
|
+
return Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildMacOSNoFileLimitWarning(currentLimit: number): string {
|
|
13
|
+
return [
|
|
14
|
+
`Warning: macOS file descriptor limit is low (ulimit -n = ${currentLimit}).`,
|
|
15
|
+
'SKC and project dev servers can hit EMFILE / "too many open files" while scanning or watching repositories.',
|
|
16
|
+
"For this terminal session, run:",
|
|
17
|
+
` ulimit -n ${RECOMMENDED_MACOS_NOFILE_LIMIT}`,
|
|
18
|
+
"If your shell refuses that value, raise the per-user launchd limit and restart the terminal:",
|
|
19
|
+
` sudo launchctl limit maxfiles ${RECOMMENDED_MACOS_NOFILE_LIMIT} 65536`,
|
|
20
|
+
"Avoid using huge values such as 2147483646 on macOS; they are commonly rejected or clamped.",
|
|
21
|
+
"Set SKC_SKIP_NOFILE_CHECK=1 to silence this preflight warning.",
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WarnIfMacOSNoFileLimitTooLowDeps {
|
|
26
|
+
platform?: NodeJS.Platform;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
execFileSync?: typeof execFileSync;
|
|
29
|
+
writeStderr?: (text: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function warnIfMacOSNoFileLimitTooLow(deps: WarnIfMacOSNoFileLimitTooLowDeps = {}): void {
|
|
33
|
+
const platform = deps.platform ?? process.platform;
|
|
34
|
+
if (platform !== "darwin") return;
|
|
35
|
+
const env = deps.env ?? process.env;
|
|
36
|
+
if (env.SKC_SKIP_NOFILE_CHECK === "1" || env.SKC_SKIP_NOFILE_CHECK === "true") return;
|
|
37
|
+
|
|
38
|
+
const run = deps.execFileSync ?? execFileSync;
|
|
39
|
+
let currentLimit: number | undefined;
|
|
40
|
+
try {
|
|
41
|
+
currentLimit = parseNoFileLimit(run("/bin/sh", ["-lc", "ulimit -n"], { encoding: "utf8" }));
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (currentLimit === undefined || currentLimit >= RECOMMENDED_MACOS_NOFILE_LIMIT) return;
|
|
46
|
+
const write = deps.writeStderr ?? (text => process.stderr.write(text));
|
|
47
|
+
write(`${buildMacOSNoFileLimitWarning(currentLimit)}\n`);
|
|
48
|
+
}
|
package/src/cli/notify-cli.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Handles `skc notify` setup/status and the hidden daemon entrypoint.
|
|
5
5
|
*/
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
|
-
import { APP_NAME } from "@sayknow-cli/utils";
|
|
7
|
+
import { APP_NAME } from "@sayknow-cli/utils/dirs";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import type { Settings } from "../config/settings";
|
|
10
10
|
import { getNotificationConfig, maskToken } from "../notifications/config";
|
|
@@ -58,6 +58,11 @@ interface TelegramUser {
|
|
|
58
58
|
allows_users_to_create_topics?: boolean;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
interface TelegramChat {
|
|
62
|
+
id?: number | string;
|
|
63
|
+
type?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
type ThreadedModeState = "enabled" | "disabled" | "unknown";
|
|
62
67
|
type ThreadedModeFinalLabel = "verified" | "unverified" | "unknown";
|
|
63
68
|
|
|
@@ -148,6 +153,7 @@ async function runSetup(deps: NotifyCommandDeps): Promise<void> {
|
|
|
148
153
|
let chatId: string;
|
|
149
154
|
if (deps.setupChatId?.trim()) {
|
|
150
155
|
chatId = deps.setupChatId.trim();
|
|
156
|
+
await verifyPrivateChatId(fetchImpl, apiBase, token, chatId);
|
|
151
157
|
process.stdout.write(`Using provided chat id ${chatId} (non-interactive).\n`);
|
|
152
158
|
} else {
|
|
153
159
|
const stale = await getUpdates(fetchImpl, apiBase, token, { timeout: 0, allowed_updates: ["message"] });
|
|
@@ -170,31 +176,91 @@ async function runSetup(deps: NotifyCommandDeps): Promise<void> {
|
|
|
170
176
|
);
|
|
171
177
|
}
|
|
172
178
|
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
type TokenPromptInput = NodeJS.ReadStream & {
|
|
180
|
+
isRaw?: boolean;
|
|
181
|
+
setRawMode?: (mode: boolean) => unknown;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type TokenPromptOutput = Pick<NodeJS.WriteStream, "write">;
|
|
185
|
+
|
|
186
|
+
export async function promptForToken(
|
|
187
|
+
input: TokenPromptInput = process.stdin,
|
|
188
|
+
output: TokenPromptOutput = process.stdout,
|
|
189
|
+
): Promise<string> {
|
|
190
|
+
if (!input.isTTY) {
|
|
175
191
|
throw new Error("notify setup requires an interactive TTY unless setupToken is injected.");
|
|
176
192
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return (await rl.question("Telegram BotFather token: ")).trim();
|
|
180
|
-
} finally {
|
|
181
|
-
rl.close();
|
|
193
|
+
if (typeof input.setRawMode !== "function") {
|
|
194
|
+
throw new Error("notify setup requires a TTY with raw input support unless setupToken is injected.");
|
|
182
195
|
}
|
|
196
|
+
|
|
197
|
+
output.write("Telegram BotFather token: ");
|
|
198
|
+
const wasRaw = input.isRaw === true;
|
|
199
|
+
input.setRawMode(true);
|
|
200
|
+
|
|
201
|
+
return await new Promise<string>((resolve, reject) => {
|
|
202
|
+
let value = "";
|
|
203
|
+
let settled = false;
|
|
204
|
+
|
|
205
|
+
const cleanup = () => {
|
|
206
|
+
input.off("data", onData);
|
|
207
|
+
input.off("error", onError);
|
|
208
|
+
input.setRawMode?.(wasRaw);
|
|
209
|
+
output.write("\n");
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const finish = (callback: () => void) => {
|
|
213
|
+
if (settled) return;
|
|
214
|
+
settled = true;
|
|
215
|
+
cleanup();
|
|
216
|
+
callback();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const accept = () => finish(() => resolve(value.trim()));
|
|
220
|
+
const cancel = () => finish(() => reject(new Error("Telegram bot token prompt cancelled.")));
|
|
221
|
+
const onError = (error: Error) => finish(() => reject(error));
|
|
222
|
+
const onData = (chunk: Buffer | string) => {
|
|
223
|
+
for (const char of String(chunk)) {
|
|
224
|
+
if (char === "\r" || char === "\n") {
|
|
225
|
+
accept();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (char === "\u0003") {
|
|
229
|
+
cancel();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (char === "\u0004") {
|
|
233
|
+
if (value) accept();
|
|
234
|
+
else cancel();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (char === "\u007f" || char === "\b") {
|
|
238
|
+
value = value.slice(0, -1);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (char >= " ") value += char;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
input.on("data", onData);
|
|
246
|
+
input.once("error", onError);
|
|
247
|
+
input.resume();
|
|
248
|
+
});
|
|
183
249
|
}
|
|
184
250
|
|
|
185
251
|
const THREADED_ENABLED_SUCCESS =
|
|
186
|
-
"Telegram Threaded Mode capability verified for this bot. SKC will request a private-chat topic per session; if Telegram ever refuses topic creation, notifications fall back to this flat chat with a one-time nudge.\n";
|
|
252
|
+
"Telegram Threaded Mode capability verified for this bot. SKC will request a private-chat topic per session; if Telegram ever refuses topic creation, notifications fall back to this flat chat with inline ask buttons only and a one-time Threaded Mode nudge.\n";
|
|
187
253
|
|
|
188
254
|
const THREADED_MISSING_WARNING =
|
|
189
|
-
"Warning: Telegram getMe did not include has_topics_enabled, so SKC cannot verify private-chat Threaded Mode capability for this bot. Setup will continue;
|
|
255
|
+
"Warning: Telegram getMe did not include has_topics_enabled, so SKC cannot verify private-chat Threaded Mode capability for this bot. Setup will continue; flat private-chat fallback supports outbound notifications and inline ask buttons only. Free-text replies and session commands require Threaded Mode/topic routing.\n";
|
|
190
256
|
|
|
191
257
|
const THREADED_NONINTERACTIVE_WARNING =
|
|
192
|
-
"Warning: Telegram Threaded Mode capability is OFF for this bot. Setup will be saved because this run is non-interactive
|
|
258
|
+
"Warning: Telegram Threaded Mode capability is OFF for this bot. Setup will be saved because this run is non-interactive. Flat private-chat fallback supports outbound notifications and inline ask buttons only; free-text replies and session commands require enabling Threaded Mode in @BotFather > Bot Settings > Threads Settings.\n";
|
|
193
259
|
|
|
194
260
|
const THREADED_DISABLED_GUIDANCE =
|
|
195
261
|
"Telegram Threaded Mode is OFF for this bot. SKC needs Telegram private-chat topics so each session can use its own thread.\n" +
|
|
196
|
-
"SKC cannot enable this through the Bot API. Open @BotFather
|
|
197
|
-
"
|
|
262
|
+
"SKC cannot enable this through the Bot API. Open @BotFather > Bot Settings > Threads Settings for this bot, enable Threaded Mode / forum topics for private chats, then return here.\n" +
|
|
263
|
+
"Without Threaded Mode, flat private-chat fallback supports outbound notifications and inline ask buttons only; free-text replies and session commands require topic routing.\n";
|
|
198
264
|
|
|
199
265
|
const THREADED_DISABLED_PROMPT =
|
|
200
266
|
"Press Enter after enabling Threaded Mode, or type skip to finish setup with a warning: ";
|
|
@@ -204,7 +270,7 @@ const THREADED_STILL_OFF = "Telegram still reports Threaded Mode OFF for this bo
|
|
|
204
270
|
const THREADED_RETRY_PROMPT = "Press Enter to check again, or type skip to finish setup with a warning: ";
|
|
205
271
|
|
|
206
272
|
const THREADED_SKIP_WARNING =
|
|
207
|
-
"Warning: continuing without verified Telegram Threaded Mode capability. Setup will be saved
|
|
273
|
+
"Warning: continuing without verified Telegram Threaded Mode capability. Setup will be saved. Flat private-chat fallback supports outbound notifications and inline ask buttons only; free-text replies and session commands require enabling Threaded Mode in BotFather.\n";
|
|
208
274
|
|
|
209
275
|
const THREADED_INVALID_INPUT = "Type Enter to retry or skip to continue with a warning.\n";
|
|
210
276
|
|
|
@@ -223,6 +289,24 @@ async function getMe(fetchImpl: typeof fetch, apiBase: string, token: string): P
|
|
|
223
289
|
return user;
|
|
224
290
|
}
|
|
225
291
|
|
|
292
|
+
async function verifyPrivateChatId(
|
|
293
|
+
fetchImpl: typeof fetch,
|
|
294
|
+
apiBase: string,
|
|
295
|
+
token: string,
|
|
296
|
+
chatId: string,
|
|
297
|
+
): Promise<void> {
|
|
298
|
+
const chat = (await callTelegram<unknown>(fetchImpl, apiBase, token, "getChat", { chat_id: chatId })) as
|
|
299
|
+
| TelegramChat
|
|
300
|
+
| undefined;
|
|
301
|
+
if (!chat || typeof chat !== "object") {
|
|
302
|
+
throw new Error("Telegram getChat returned invalid Telegram response: missing valid Chat result.");
|
|
303
|
+
}
|
|
304
|
+
if (chat.type !== "private") {
|
|
305
|
+
const type = typeof chat.type === "string" && chat.type ? chat.type : "unknown";
|
|
306
|
+
throw new Error(`Provided chat id ${chatId} is a ${type} chat; pairing requires a private Telegram chat.`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
226
310
|
function threadedModeState(user: TelegramUser): ThreadedModeState {
|
|
227
311
|
if (user.has_topics_enabled === true) return "enabled";
|
|
228
312
|
if (user.has_topics_enabled === false) return "disabled";
|
|
@@ -416,8 +500,10 @@ ${chalk.bold("Examples:")}
|
|
|
416
500
|
|
|
417
501
|
${chalk.bold("Threaded Mode:")}
|
|
418
502
|
SKC uses Telegram private-chat topics for per-session threads. Setup verifies the bot
|
|
419
|
-
capability via getMe.has_topics_enabled.
|
|
420
|
-
bots cannot toggle it through the Bot API. If Telegram refuses topic
|
|
421
|
-
SKC delivers flat to the paired private chat
|
|
503
|
+
capability via getMe.has_topics_enabled. Enable Threaded Mode in @BotFather > Bot Settings
|
|
504
|
+
> Threads Settings; bots cannot toggle it through the Bot API. If Telegram refuses topic
|
|
505
|
+
creation at runtime, SKC delivers flat to the paired private chat with outbound notifications
|
|
506
|
+
and inline ask buttons only, then nudges you to enable Threaded Mode for free-text replies
|
|
507
|
+
and session commands.
|
|
422
508
|
`);
|
|
423
509
|
}
|
package/src/cli/telegram-cli.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { logger } from "@sayknow-cli/utils";
|
|
7
|
-
import { resolveSkcRuntimeSpawnInfo } from "../daemon/runtime";
|
|
8
7
|
import { isSettingsInitialized, Settings, settings } from "../config/settings";
|
|
9
8
|
import type { TelegramSettings } from "../config/settings-schema";
|
|
10
9
|
import { telegramSettingsToEnv, validateTelegramSettings } from "../config/telegram-env-bridge";
|
|
10
|
+
import { resolveSkcRuntimeSpawnInfo } from "../daemon/runtime";
|
|
11
11
|
|
|
12
12
|
export type TelegramAction = "start" | "status" | "env" | "__gateway";
|
|
13
13
|
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -14,9 +14,10 @@ import { installDefaultSkcDefinitions } from "../defaults/skc-defaults";
|
|
|
14
14
|
import { theme } from "../modes/theme/theme";
|
|
15
15
|
|
|
16
16
|
const RELEASE_REPO = "jaybeyond/Sayknow_CLI";
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
17
|
+
const RELEASE_TAG_PREFIX = "sayknow-v";
|
|
18
|
+
const PACKAGE = "sayknow-cli";
|
|
19
|
+
const SCOPED_CLI_PACKAGE = "@sayknow-cli/coding-agent";
|
|
20
|
+
const NPM_MANAGED_PACKAGES = [PACKAGE, SCOPED_CLI_PACKAGE] as const;
|
|
20
21
|
|
|
21
22
|
interface ReleaseInfo {
|
|
22
23
|
tag: string;
|
|
@@ -136,35 +137,79 @@ function defaultPackageExists(_packageName: string, packageRoot: string): boolea
|
|
|
136
137
|
return fs.existsSync(path.join(packageRoot, "package.json"));
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
function
|
|
140
|
+
function npmPackageRootForWindowsShim(binPath: string, packageName: string, platform: PathPlatform): string {
|
|
140
141
|
const pathApi = pathApiForPlatform(platform);
|
|
141
142
|
const segments = packageName.split("/");
|
|
142
143
|
return pathApi.join(pathApi.dirname(binPath), "node_modules", ...segments);
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
function packageRootFromNodeModulesPath(
|
|
147
|
+
binPath: string,
|
|
148
|
+
packageName: string,
|
|
149
|
+
platform: PathPlatform,
|
|
150
|
+
): string | undefined {
|
|
151
|
+
const pathApi = pathApiForPlatform(platform);
|
|
152
|
+
const packageSegments = packageName.split("/");
|
|
153
|
+
const parts = pathApi.normalize(binPath).split(pathApi.sep).filter(Boolean);
|
|
154
|
+
for (let index = 0; index < parts.length; index++) {
|
|
155
|
+
if (parts[index] !== "node_modules") continue;
|
|
156
|
+
const packageStart = index + 1;
|
|
157
|
+
const matched = packageSegments.every((segment, offset) => parts[packageStart + offset] === segment);
|
|
158
|
+
if (!matched) continue;
|
|
159
|
+
const prefix = binPath.startsWith(pathApi.sep) ? pathApi.sep : "";
|
|
160
|
+
return pathApi.join(prefix, ...parts.slice(0, packageStart + packageSegments.length));
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function npmPackageRootsForPosixShim(binPath: string, packageName: string): string[] {
|
|
166
|
+
const pathApi = path.posix;
|
|
167
|
+
const binDir = pathApi.dirname(binPath);
|
|
168
|
+
const packageSegments = packageName.split("/");
|
|
169
|
+
return [
|
|
170
|
+
pathApi.join(pathApi.dirname(binDir), "lib", "node_modules", ...packageSegments),
|
|
171
|
+
pathApi.join(binDir, "node_modules", ...packageSegments),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
145
175
|
function resolveNpmManagedTarget(
|
|
146
176
|
ompPath: string,
|
|
147
177
|
platform: PathPlatform = process.platform,
|
|
148
178
|
packageExists: PackageExists = defaultPackageExists,
|
|
149
179
|
): PackageManagerTarget | undefined {
|
|
150
|
-
if (platform !== "win32") return undefined;
|
|
151
180
|
const pathApi = pathApiForPlatform(platform);
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
181
|
+
const basename = pathApi.basename(ompPath, pathApi.extname(ompPath)).toLowerCase();
|
|
182
|
+
const resolvedPath = tryRealpath(ompPath);
|
|
183
|
+
const candidatePaths = resolvedPath ? [resolvedPath, ompPath] : [ompPath];
|
|
184
|
+
|
|
185
|
+
if (platform === "win32") {
|
|
186
|
+
const extension = pathApi.extname(ompPath).toLowerCase();
|
|
187
|
+
if (extension !== ".cmd" && extension !== ".ps1") return undefined;
|
|
188
|
+
if (basename !== APP_NAME.toLowerCase()) return undefined;
|
|
189
|
+
for (const packageName of NPM_MANAGED_PACKAGES) {
|
|
190
|
+
const packageRoot = npmPackageRootForWindowsShim(ompPath, packageName, platform);
|
|
191
|
+
if (packageExists(packageName, packageRoot)) return { manager: "npm", packageName };
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
156
195
|
|
|
196
|
+
if (basename !== APP_NAME.toLowerCase()) return undefined;
|
|
157
197
|
for (const packageName of NPM_MANAGED_PACKAGES) {
|
|
158
|
-
const
|
|
159
|
-
|
|
198
|
+
for (const candidatePath of candidatePaths) {
|
|
199
|
+
const packageRoot = packageRootFromNodeModulesPath(candidatePath, packageName, platform);
|
|
200
|
+
if (packageRoot && packageExists(packageName, packageRoot)) return { manager: "npm", packageName };
|
|
201
|
+
}
|
|
202
|
+
for (const packageRoot of npmPackageRootsForPosixShim(ompPath, packageName)) {
|
|
203
|
+
if (packageExists(packageName, packageRoot)) return { manager: "npm", packageName };
|
|
204
|
+
}
|
|
160
205
|
}
|
|
161
206
|
return undefined;
|
|
162
207
|
}
|
|
163
208
|
|
|
164
209
|
function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary" {
|
|
210
|
+
if (bunBinDir && isPathInDirectory(ompPath, bunBinDir)) return "bun";
|
|
165
211
|
if (resolveNpmManagedTarget(ompPath)) return "npm";
|
|
166
|
-
|
|
167
|
-
return isPathInDirectory(ompPath, bunBinDir) ? "bun" : "binary";
|
|
212
|
+
return "binary";
|
|
168
213
|
}
|
|
169
214
|
|
|
170
215
|
export function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary" {
|
|
@@ -183,16 +228,10 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
183
228
|
const ompPath = resolveSkcPath();
|
|
184
229
|
|
|
185
230
|
if (ompPath) {
|
|
231
|
+
if (bunBinDir && isPathInDirectory(ompPath, bunBinDir)) return { method: "bun" };
|
|
186
232
|
const npmTarget = resolveNpmManagedTarget(ompPath);
|
|
187
233
|
if (npmTarget) return { method: "npm", packageName: npmTarget.packageName };
|
|
188
|
-
|
|
189
|
-
if (method === "bun") return { method };
|
|
190
|
-
if (method === "npm") {
|
|
191
|
-
throw new Error(
|
|
192
|
-
formatUnsupportedTargetMessage(`Could not resolve npm package root for ${APP_NAME} shim ${ompPath}`),
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
return { method, path: ompPath };
|
|
234
|
+
return { method: "binary", path: ompPath };
|
|
196
235
|
}
|
|
197
236
|
|
|
198
237
|
if (bunBinDir) return { method: "bun" };
|
|
@@ -212,7 +251,7 @@ async function getLatestRelease(): Promise<ReleaseInfo> {
|
|
|
212
251
|
|
|
213
252
|
const data = (await response.json()) as { version: string };
|
|
214
253
|
const version = data.version;
|
|
215
|
-
const tag =
|
|
254
|
+
const tag = `${RELEASE_TAG_PREFIX}${version}`;
|
|
216
255
|
|
|
217
256
|
return {
|
|
218
257
|
tag,
|
|
@@ -350,7 +389,7 @@ function formatBinaryInstallInstruction(platform: NodeJS.Platform = process.plat
|
|
|
350
389
|
function formatManualUpdateInstructions(platform: NodeJS.Platform = process.platform): string {
|
|
351
390
|
return [
|
|
352
391
|
`If ${APP_NAME} was installed with Bun, run: bun install -g ${PACKAGE}@latest`,
|
|
353
|
-
`If ${APP_NAME} was installed with npm, pnpm, or another package manager,
|
|
392
|
+
`If ${APP_NAME} was installed with npm, pnpm, or another package manager, run: npm install -g ${PACKAGE}@latest`,
|
|
354
393
|
formatBinaryInstallInstruction(platform),
|
|
355
394
|
].join("\n");
|
|
356
395
|
}
|
|
@@ -365,7 +404,7 @@ function buildReleaseBinaryUrl(
|
|
|
365
404
|
arch: string = process.arch,
|
|
366
405
|
): string {
|
|
367
406
|
const binaryName = getBinaryName(platform, arch);
|
|
368
|
-
const tag =
|
|
407
|
+
const tag = `${RELEASE_TAG_PREFIX}${version}`;
|
|
369
408
|
return `https://github.com/${RELEASE_REPO}/releases/download/${tag}/${binaryName}`;
|
|
370
409
|
}
|
|
371
410
|
|
|
@@ -552,31 +591,107 @@ async function updateViaNpm(packageName: string, expectedVersion: string): Promi
|
|
|
552
591
|
}
|
|
553
592
|
|
|
554
593
|
/**
|
|
555
|
-
*
|
|
594
|
+
* Flush a freshly written file's data to stable storage.
|
|
595
|
+
*
|
|
596
|
+
* Critical on network filesystems (e.g. NFS home directories): `pipeline`
|
|
597
|
+
* resolving does not guarantee the downloaded bytes are durable on the
|
|
598
|
+
* server, so the post-install `skc --version` check can exec a binary whose
|
|
599
|
+
* pages are not yet consistent. The child then faults, the version check
|
|
600
|
+
* fails, and the update is rolled back with "could not verify updated
|
|
601
|
+
* version" even though the download itself succeeded. Explicitly fsyncing
|
|
602
|
+
* before the rename/exec avoids the race.
|
|
556
603
|
*/
|
|
557
|
-
async function
|
|
558
|
-
const
|
|
559
|
-
|
|
604
|
+
async function fsyncFile(filePath: string): Promise<void> {
|
|
605
|
+
const handle = await fs.promises.open(filePath, "r+");
|
|
606
|
+
try {
|
|
607
|
+
await handle.sync();
|
|
608
|
+
} finally {
|
|
609
|
+
await handle.close();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
560
612
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
613
|
+
export async function fsyncFileForTest(filePath: string): Promise<void> {
|
|
614
|
+
return fsyncFile(filePath);
|
|
615
|
+
}
|
|
564
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Download a release binary to a temp path, throwing a friendly error when the
|
|
619
|
+
* release asset cannot be fetched.
|
|
620
|
+
*/
|
|
621
|
+
async function downloadBinaryTo(url: string, tempPath: string, binaryName: string): Promise<void> {
|
|
565
622
|
const response = await fetch(url, { redirect: "follow" });
|
|
566
623
|
if (!response.ok || !response.body) {
|
|
567
624
|
throw new Error(formatBinaryDownloadFailureMessage(binaryName, url, response.statusText || response.status));
|
|
568
625
|
}
|
|
569
626
|
const fileStream = fs.createWriteStream(tempPath, { mode: 0o755 });
|
|
570
627
|
await pipeline(response.body, fileStream);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** Injectable steps of the binary update flow (seams for testing ordering). */
|
|
631
|
+
export interface BinaryUpdateFlow {
|
|
632
|
+
download(url: string, tempPath: string): Promise<void>;
|
|
633
|
+
fsync(filePath: string): Promise<void>;
|
|
634
|
+
replace(options: BinaryReplacementOptions): Promise<InstalledVersionVerification>;
|
|
635
|
+
verifyInstalledVersion(expectedVersion: string): Promise<InstalledVersionVerification>;
|
|
636
|
+
/** Best-effort cleanup of the temp file when the flow aborts before replace. */
|
|
637
|
+
removeTemp?(filePath: string): Promise<void>;
|
|
638
|
+
/** Called once fsync has succeeded, right before replacement begins. */
|
|
639
|
+
beforeReplace?(): void;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Orchestrate download → fsync → replace → verify with a strict ordering
|
|
644
|
+
* contract: the downloaded temp binary MUST be flushed to stable storage
|
|
645
|
+
* before it is published (renamed into place) or exec'd for verification.
|
|
646
|
+
*
|
|
647
|
+
* If fsync fails the temp bytes are not durable, so we abort before
|
|
648
|
+
* replacement/verification and clean up the temp file rather than installing a
|
|
649
|
+
* possibly-truncated binary.
|
|
650
|
+
*/
|
|
651
|
+
export async function runBinaryUpdateFlow(
|
|
652
|
+
targetPath: string,
|
|
653
|
+
url: string,
|
|
654
|
+
expectedVersion: string,
|
|
655
|
+
flow: BinaryUpdateFlow,
|
|
656
|
+
): Promise<InstalledVersionVerification> {
|
|
657
|
+
const tempPath = `${targetPath}.new`;
|
|
658
|
+
const backupPath = `${targetPath}.bak`;
|
|
659
|
+
|
|
660
|
+
await flow.download(url, tempPath);
|
|
661
|
+
try {
|
|
662
|
+
await flow.fsync(tempPath);
|
|
663
|
+
} catch (err) {
|
|
664
|
+
if (flow.removeTemp) await flow.removeTemp(tempPath);
|
|
665
|
+
throw err;
|
|
666
|
+
}
|
|
571
667
|
|
|
572
|
-
|
|
573
|
-
|
|
668
|
+
flow.beforeReplace?.();
|
|
669
|
+
return flow.replace({
|
|
574
670
|
targetPath,
|
|
575
671
|
tempPath,
|
|
576
672
|
backupPath,
|
|
577
673
|
expectedVersion,
|
|
674
|
+
verifyInstalledVersion: flow.verifyInstalledVersion,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Download a release binary to a target path, replacing an existing file.
|
|
680
|
+
*/
|
|
681
|
+
async function updateViaBinaryAt(targetPath: string, expectedVersion: string): Promise<void> {
|
|
682
|
+
const binaryName = getBinaryName();
|
|
683
|
+
const url = buildReleaseBinaryUrl(expectedVersion);
|
|
684
|
+
console.log(chalk.dim(`Downloading ${binaryName}…`));
|
|
685
|
+
|
|
686
|
+
const verification = await runBinaryUpdateFlow(targetPath, url, expectedVersion, {
|
|
687
|
+
download: (downloadUrl, tempPath) => downloadBinaryTo(downloadUrl, tempPath, binaryName),
|
|
688
|
+
fsync: fsyncFile,
|
|
689
|
+
replace: replaceBinaryForUpdate,
|
|
578
690
|
verifyInstalledVersion: verifyInstalledRuntime,
|
|
691
|
+
removeTemp: unlinkIfExists,
|
|
692
|
+
beforeReplace: () => console.log(chalk.dim("Installing update...")),
|
|
579
693
|
});
|
|
694
|
+
|
|
580
695
|
printVerifiedVersion(expectedVersion);
|
|
581
696
|
if (verification.cleanupWarning) console.warn(chalk.yellow(verification.cleanupWarning));
|
|
582
697
|
printRestartGuidance();
|