@gajae-code/coding-agent 0.18.12 → 0.19.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/CHANGELOG.md +467 -0
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +7 -2
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +8 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +23 -1
- package/dist/types/config/config-hot-reload.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +44 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -5
- package/dist/types/config/model-registry.d.ts +16 -3
- package/dist/types/config/model-resolver.d.ts +5 -4
- package/dist/types/config/settings-schema.d.ts +11 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +9 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +1 -1
- package/dist/types/migrate/adapters/index.d.ts +35 -3
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +69 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +22 -1
- package/dist/types/sdk/broker/ensure.d.ts +5 -2
- package/dist/types/sdk/broker/hop.d.ts +9 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/session-index.d.ts +2 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/index.d.ts +21 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +8 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/session/agent-session.d.ts +50 -8
- package/dist/types/session/session-manager.d.ts +16 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +13 -2
- package/dist/types/tools/fetch.d.ts +6 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/package.json +9 -8
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/generate-sdk-operation-inventory.ts +16 -0
- package/src/capability/fs.ts +31 -3
- package/src/capability/rule.ts +11 -2
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/fast-help.ts +0 -2
- package/src/cli/migrate-cli.ts +14 -3
- package/src/cli/public-command-errors.ts +2 -0
- package/src/cli/root-flags.ts +0 -2
- package/src/cli/update-cli.ts +247 -6
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +81 -11
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +20 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload.ts +8 -0
- package/src/config/file-lock.ts +1431 -304
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-profile-activation.ts +177 -39
- package/src/config/model-registry.ts +380 -55
- package/src/config/model-resolver.ts +49 -21
- package/src/config/settings-schema.ts +13 -1
- package/src/config/settings.ts +2 -0
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +3 -1
- package/src/customization/import.ts +276 -7
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +87 -5
- package/src/debug/report-bundle.ts +71 -2
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +24 -14
- package/src/edit/index.ts +191 -5
- package/src/edit/modes/patch.ts +27 -0
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +60 -20
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/extensions/loader.ts +2 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +4 -1
- package/src/gjc-runtime/launch-tmux.ts +23 -13
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +3 -1
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +14 -14
- package/src/internal-urls/local-protocol.ts +28 -3
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +517 -2
- package/src/migrate/adapters/claude-code.ts +141 -3
- package/src/migrate/adapters/codex.ts +57 -5
- package/src/migrate/adapters/index.ts +240 -5
- package/src/migrate/adapters/opencode.ts +4 -0
- package/src/migrate/executor.ts +141 -3
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +45 -1
- package/src/migrate/types.ts +73 -3
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +271 -16
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +49 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +14 -1
- package/src/modes/interactive-mode.ts +55 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/runtime-mcp/manager.ts +8 -3
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/tool-bridge.ts +68 -4
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +2 -0
- package/src/sdk/broker/broker.ts +143 -4
- package/src/sdk/broker/ensure.ts +74 -6
- package/src/sdk/broker/hop.ts +13 -1
- package/src/sdk/broker/lifecycle-ledger.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/session-index.ts +29 -4
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +408 -14
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1537 -24
- package/src/sdk/host/session-runtime.ts +332 -36
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +14 -1
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +28 -1
- package/src/sdk/protocol/operation-inventory.generated.json +160 -0
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/session.ts +123 -36
- package/src/session/agent-session.ts +1614 -329
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +47 -18
- package/src/session/internal/managed-session-storage.ts +71 -1
- package/src/session/session-manager.ts +90 -5
- package/src/session-import/redact.ts +53 -2
- package/src/skill-state/workflow-mutation-guard.ts +1498 -18
- package/src/slash-commands/builtin-registry.ts +6 -3
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +12 -8
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/auto-generated-guard.ts +12 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +61 -1
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +99 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +9 -2
- package/src/tools/descriptors.test.ts +4 -4
- package/src/tools/descriptors.ts +16 -4
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +90 -2
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +51 -3
- package/src/tools/read.ts +9 -2
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent.ts +10 -4
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +7 -0
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +3 -2
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +46 -6
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +65 -20
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* discovery/policy model (trust, include/ignore, disabledExtensions,
|
|
7
7
|
* bundled-name protection). A narrow supplemental scan flags SKILL.md files
|
|
8
8
|
* the management loader rejects as `invalid` rows.
|
|
9
|
-
* - Hooks: canonical `<root>/hooks/<pre|post>/` layout
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - Hooks: canonical `<root>/hooks/<pre|post>/` layout. Project hooks are
|
|
10
|
+
* stored-only and do not shadow user hooks because sessions never load
|
|
11
|
+
* repository hook modules. User-scope collisions follow the runtime's
|
|
12
|
+
* first-wins dedupe key (`<type>:<tool>:<name>`); losers are `shadowed`.
|
|
12
13
|
* - MCPs: canonical config files for both scopes with the runtime's
|
|
13
14
|
* project-first precedence and the union of disabled markers; malformed
|
|
14
15
|
* files surface as `invalid` rows without leaking raw JSON.
|
|
@@ -173,7 +174,8 @@ const HOOK_PHASES = ["pre", "post"] as const;
|
|
|
173
174
|
|
|
174
175
|
async function loadHookRows(options: LoadCustomizationInventoryOptions, _warnings: string[]): Promise<InventoryRow[]> {
|
|
175
176
|
const rows: InventoryRow[] = [];
|
|
176
|
-
//
|
|
177
|
+
// Repository hooks are deliberately excluded by session startup. They stay
|
|
178
|
+
// visible for management, but are stored-only and cannot shadow user hooks.
|
|
177
179
|
const seen = new Set<string>();
|
|
178
180
|
for (const scope of ["project", "global"] as const) {
|
|
179
181
|
const hooksDir = resolveScopePaths(scope, options.cwd).hooksDir;
|
|
@@ -190,19 +192,33 @@ async function loadHookRows(options: LoadCustomizationInventoryOptions, _warning
|
|
|
190
192
|
const hookPath = path.join(phaseDir, entry.name);
|
|
191
193
|
const baseName = entry.name.includes(".") ? entry.name.slice(0, entry.name.lastIndexOf(".")) : entry.name;
|
|
192
194
|
const key = `${phase}:${baseName}:${entry.name}`;
|
|
193
|
-
const
|
|
194
|
-
seen.
|
|
195
|
+
const storedOnly = scope === "project";
|
|
196
|
+
const shadowed = !storedOnly && seen.has(key);
|
|
197
|
+
if (!storedOnly) seen.add(key);
|
|
198
|
+
let status: InventoryStatus;
|
|
199
|
+
let diagnostics: string[] | undefined;
|
|
200
|
+
if (storedOnly) {
|
|
201
|
+
status = "stored-only";
|
|
202
|
+
diagnostics = [
|
|
203
|
+
"project hooks are stored only; sessions do not load repository hook modules automatically",
|
|
204
|
+
];
|
|
205
|
+
} else if (shadowed) {
|
|
206
|
+
status = "shadowed";
|
|
207
|
+
diagnostics = ["shadowed by the same hook at higher-precedence scope"];
|
|
208
|
+
} else {
|
|
209
|
+
status = "enabled";
|
|
210
|
+
}
|
|
195
211
|
rows.push({
|
|
196
212
|
id: inventoryRowId("hooks", scope, hookPath),
|
|
197
213
|
surface: "hooks",
|
|
198
214
|
name: entry.name,
|
|
199
215
|
displayName: `${phase}/${entry.name}`,
|
|
200
|
-
status
|
|
216
|
+
status,
|
|
201
217
|
provenance: `${scopeLabel(scope)} hooks/${phase}`,
|
|
202
218
|
path: hookPath,
|
|
203
219
|
scope,
|
|
204
220
|
description: `${phase}-tool hook for ${baseName}`,
|
|
205
|
-
diagnostics
|
|
221
|
+
diagnostics,
|
|
206
222
|
raw: { name: entry.name, type: phase, tool: baseName },
|
|
207
223
|
});
|
|
208
224
|
}
|
package/src/dap/client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import { logger } from "@gajae-code/utils";
|
|
4
|
+
import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
|
|
4
5
|
import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
|
|
5
6
|
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
6
7
|
import { type OwnedProcess, spawnOwnedProcess } from "../runtime/process-lifecycle";
|
|
@@ -134,7 +135,7 @@ export class DapClient {
|
|
|
134
135
|
// debuggee children can reach /dev/tty and trigger SIGTTIN, suspending
|
|
135
136
|
// the parent harness under shell job control.
|
|
136
137
|
const env = {
|
|
137
|
-
...
|
|
138
|
+
...projectSafeSpawnEnv(),
|
|
138
139
|
...NON_INTERACTIVE_ENV,
|
|
139
140
|
};
|
|
140
141
|
const owner = spawnOwnedProcess([adapter.resolvedCommand, ...adapter.args], {
|
|
@@ -159,7 +160,7 @@ export class DapClient {
|
|
|
159
160
|
*/
|
|
160
161
|
static async #spawnSocket({ adapter, cwd }: DapSpawnOptions): Promise<DapClient> {
|
|
161
162
|
const env = {
|
|
162
|
-
...
|
|
163
|
+
...projectSafeSpawnEnv(),
|
|
163
164
|
...NON_INTERACTIVE_ENV,
|
|
164
165
|
};
|
|
165
166
|
const isLinux = process.platform === "linux";
|
package/src/dap/session.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import * as timers from "node:timers/promises";
|
|
3
4
|
import { logger, untilAborted } from "@gajae-code/utils";
|
|
5
|
+
import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
|
|
4
6
|
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
5
7
|
import { type OwnedProcess, spawnOwnedProcess } from "../runtime/process-lifecycle";
|
|
6
8
|
import { DapClient } from "./client";
|
|
@@ -106,9 +108,19 @@ interface DapSession {
|
|
|
106
108
|
initializedSeen: boolean;
|
|
107
109
|
needsConfigurationDone: boolean;
|
|
108
110
|
configurationDoneSent: boolean;
|
|
111
|
+
/** Containment root. Distinct from `cwd`, which is the caller execution directory. */
|
|
112
|
+
workspace: string;
|
|
113
|
+
closing: boolean;
|
|
114
|
+
disposed: boolean;
|
|
115
|
+
inflightTerminal: number;
|
|
116
|
+
terminalIdle?: Promise<void>;
|
|
117
|
+
resolveTerminalIdle?: () => void;
|
|
109
118
|
runInTerminalProcesses: Set<OwnedProcess>;
|
|
110
119
|
}
|
|
111
120
|
|
|
121
|
+
/** Test seam: pauses after cwd containment and before the debuggee spawn. */
|
|
122
|
+
export const runInTerminalTestHooks: { beforeSpawn?: () => Promise<void> } = {};
|
|
123
|
+
|
|
112
124
|
export interface DapOutputSnapshot {
|
|
113
125
|
snapshot: DapSessionSummary;
|
|
114
126
|
output: string;
|
|
@@ -210,6 +222,53 @@ function buildSummary(session: DapSession): DapSessionSummary {
|
|
|
210
222
|
};
|
|
211
223
|
}
|
|
212
224
|
|
|
225
|
+
function isEnoent(error: unknown): boolean {
|
|
226
|
+
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Walk missing leaves so a not-yet-created debug cwd can still be contained. */
|
|
230
|
+
async function canonicalizeRunInTerminalCwd(absolute: string): Promise<string> {
|
|
231
|
+
try {
|
|
232
|
+
return await fs.realpath(absolute);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (!isEnoent(error)) throw error;
|
|
235
|
+
}
|
|
236
|
+
const missing: string[] = [];
|
|
237
|
+
let cursor = absolute;
|
|
238
|
+
for (;;) {
|
|
239
|
+
const parent = path.dirname(cursor);
|
|
240
|
+
if (parent === cursor) throw new Error("runInTerminal cwd escapes the workspace");
|
|
241
|
+
missing.unshift(path.basename(cursor));
|
|
242
|
+
cursor = parent;
|
|
243
|
+
try {
|
|
244
|
+
return path.join(await fs.realpath(cursor), ...missing);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (!isEnoent(error)) throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function cwdEscapesWorkspace(root: string, candidate: string): boolean {
|
|
252
|
+
const relative = path.relative(root, candidate);
|
|
253
|
+
return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The validated cwd is the cwd that is spawned. Relative paths resolve against
|
|
258
|
+
* the session workspace, then both sides are canonicalized before containment.
|
|
259
|
+
*/
|
|
260
|
+
async function resolveRunInTerminalCwd(workspace: string, requested: string | undefined): Promise<string> {
|
|
261
|
+
if (typeof requested === "string" && requested.trim() === "") {
|
|
262
|
+
throw new Error("runInTerminal cwd escapes the workspace");
|
|
263
|
+
}
|
|
264
|
+
const root = await fs.realpath(workspace);
|
|
265
|
+
const candidate = await canonicalizeRunInTerminalCwd(path.resolve(workspace, requested ?? workspace));
|
|
266
|
+
if (cwdEscapesWorkspace(root, candidate)) {
|
|
267
|
+
throw new Error("runInTerminal cwd escapes the workspace");
|
|
268
|
+
}
|
|
269
|
+
return candidate;
|
|
270
|
+
}
|
|
271
|
+
|
|
213
272
|
export class DapSessionManager {
|
|
214
273
|
#sessions = new Map<string, DapSession>();
|
|
215
274
|
#activeSessionId: string | null = null;
|
|
@@ -234,13 +293,19 @@ export class DapSessionManager {
|
|
|
234
293
|
}
|
|
235
294
|
|
|
236
295
|
async launch(
|
|
237
|
-
options: DapLaunchSessionOptions,
|
|
296
|
+
options: DapLaunchSessionOptions & { workspace?: string },
|
|
238
297
|
signal?: AbortSignal,
|
|
239
298
|
timeoutMs: number = 30_000,
|
|
240
299
|
): Promise<DapSessionSummary> {
|
|
241
300
|
await this.#ensureLaunchSlot();
|
|
242
301
|
const client = await DapClient.spawn({ adapter: options.adapter, cwd: options.cwd });
|
|
243
|
-
const session = this.#registerSession(
|
|
302
|
+
const session = this.#registerSession(
|
|
303
|
+
client,
|
|
304
|
+
options.adapter,
|
|
305
|
+
options.cwd,
|
|
306
|
+
options.workspace ?? options.cwd,
|
|
307
|
+
options.program,
|
|
308
|
+
);
|
|
244
309
|
try {
|
|
245
310
|
session.capabilities = await client.initialize(
|
|
246
311
|
this.#buildInitializeArguments(options.adapter),
|
|
@@ -299,13 +364,13 @@ export class DapSessionManager {
|
|
|
299
364
|
}
|
|
300
365
|
|
|
301
366
|
async attach(
|
|
302
|
-
options: DapAttachSessionOptions,
|
|
367
|
+
options: DapAttachSessionOptions & { workspace?: string },
|
|
303
368
|
signal?: AbortSignal,
|
|
304
369
|
timeoutMs: number = 30_000,
|
|
305
370
|
): Promise<DapSessionSummary> {
|
|
306
371
|
await this.#ensureLaunchSlot();
|
|
307
372
|
const client = await DapClient.spawn({ adapter: options.adapter, cwd: options.cwd });
|
|
308
|
-
const session = this.#registerSession(client, options.adapter, options.cwd);
|
|
373
|
+
const session = this.#registerSession(client, options.adapter, options.cwd, options.workspace ?? options.cwd);
|
|
309
374
|
try {
|
|
310
375
|
session.capabilities = await client.initialize(
|
|
311
376
|
this.#buildInitializeArguments(options.adapter),
|
|
@@ -886,24 +951,29 @@ export class DapSessionManager {
|
|
|
886
951
|
const session = this.#getActiveSessionOrNull();
|
|
887
952
|
if (!session) return null;
|
|
888
953
|
session.lastUsedAt = Date.now();
|
|
889
|
-
|
|
890
|
-
|
|
954
|
+
// Close before the first await. A runInTerminal paused on cwd resolution
|
|
955
|
+
// must not spawn while terminate or disconnect is still outstanding.
|
|
956
|
+
session.closing = true;
|
|
957
|
+
try {
|
|
958
|
+
if (session.status !== "terminated") {
|
|
959
|
+
if (session.capabilities?.supportsTerminateRequest) {
|
|
960
|
+
await untilAborted(
|
|
961
|
+
signal,
|
|
962
|
+
session.client.sendRequest("terminate", undefined, signal, timeoutMs).catch(() => undefined),
|
|
963
|
+
);
|
|
964
|
+
}
|
|
891
965
|
await untilAborted(
|
|
892
966
|
signal,
|
|
893
|
-
session.client
|
|
967
|
+
session.client
|
|
968
|
+
.sendRequest("disconnect", { terminateDebuggee: true }, signal, timeoutMs)
|
|
969
|
+
.catch(() => undefined),
|
|
894
970
|
);
|
|
895
971
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
.catch(() => undefined),
|
|
901
|
-
);
|
|
972
|
+
session.status = "terminated";
|
|
973
|
+
return buildSummary(session);
|
|
974
|
+
} finally {
|
|
975
|
+
await this.#disposeSession(session);
|
|
902
976
|
}
|
|
903
|
-
session.status = "terminated";
|
|
904
|
-
const summary = buildSummary(session);
|
|
905
|
-
await this.#disposeSession(session);
|
|
906
|
-
return summary;
|
|
907
977
|
}
|
|
908
978
|
|
|
909
979
|
#startCleanupTimer(): void {
|
|
@@ -945,12 +1015,22 @@ export class DapSessionManager {
|
|
|
945
1015
|
throw new Error(`Debug session ${active.id} is still active. Terminate it before launching another.`);
|
|
946
1016
|
}
|
|
947
1017
|
|
|
948
|
-
#registerSession(
|
|
1018
|
+
#registerSession(
|
|
1019
|
+
client: DapClient,
|
|
1020
|
+
adapter: DapResolvedAdapter,
|
|
1021
|
+
cwd: string,
|
|
1022
|
+
workspace: string,
|
|
1023
|
+
program?: string,
|
|
1024
|
+
): DapSession {
|
|
949
1025
|
const session: DapSession = {
|
|
950
1026
|
id: `debug-${++this.#nextId}`,
|
|
951
1027
|
adapter,
|
|
952
1028
|
cwd,
|
|
1029
|
+
workspace,
|
|
953
1030
|
program,
|
|
1031
|
+
closing: false,
|
|
1032
|
+
disposed: false,
|
|
1033
|
+
inflightTerminal: 0,
|
|
954
1034
|
client,
|
|
955
1035
|
status: "launching",
|
|
956
1036
|
launchedAt: Date.now(),
|
|
@@ -975,24 +1055,40 @@ export class DapSessionManager {
|
|
|
975
1055
|
if (!Array.isArray(args.args) || args.args.length === 0) {
|
|
976
1056
|
throw new Error("runInTerminal request did not include a command");
|
|
977
1057
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1058
|
+
if (session.closing) throw new Error("runInTerminal session is closed");
|
|
1059
|
+
this.#beginTerminal(session);
|
|
1060
|
+
try {
|
|
1061
|
+
if (session.closing) throw new Error("runInTerminal session is closed");
|
|
1062
|
+
const env = Object.fromEntries(
|
|
1063
|
+
Object.entries(args.env ?? {}).filter((entry): entry is [string, string] => entry[1] !== null),
|
|
1064
|
+
);
|
|
1065
|
+
const cwd = await resolveRunInTerminalCwd(session.workspace, args.cwd ?? session.cwd);
|
|
1066
|
+
await runInTerminalTestHooks.beforeSpawn?.();
|
|
1067
|
+
if (session.closing || !this.#sessions.has(session.id)) {
|
|
1068
|
+
throw new Error("runInTerminal session is closed");
|
|
1069
|
+
}
|
|
1070
|
+
const owner = spawnOwnedProcess(args.args, {
|
|
1071
|
+
cwd,
|
|
1072
|
+
stdin: "ignore",
|
|
1073
|
+
env: {
|
|
1074
|
+
...projectSafeSpawnEnv(),
|
|
1075
|
+
...NON_INTERACTIVE_ENV,
|
|
1076
|
+
...env,
|
|
1077
|
+
},
|
|
1078
|
+
name: `dap:${session.id}:runInTerminal`,
|
|
1079
|
+
});
|
|
1080
|
+
if (session.closing || !this.#sessions.has(session.id)) {
|
|
1081
|
+
await owner.dispose();
|
|
1082
|
+
throw new Error("runInTerminal session is closed");
|
|
1083
|
+
}
|
|
1084
|
+
drainStream(owner.child.stdout);
|
|
1085
|
+
drainStream(owner.child.stderr);
|
|
1086
|
+
session.runInTerminalProcesses.add(owner);
|
|
1087
|
+
owner.exited.finally(() => session.runInTerminalProcesses.delete(owner));
|
|
1088
|
+
return { processId: owner.pid } satisfies DapRunInTerminalResponse;
|
|
1089
|
+
} finally {
|
|
1090
|
+
this.#endTerminal(session);
|
|
1091
|
+
}
|
|
996
1092
|
});
|
|
997
1093
|
client.onReverseRequest("startDebugging", async rawArgs => {
|
|
998
1094
|
const startArgs = (rawArgs ?? {}) as Partial<DapStartDebuggingArguments>;
|
|
@@ -1319,7 +1415,36 @@ export class DapSessionManager {
|
|
|
1319
1415
|
return session;
|
|
1320
1416
|
}
|
|
1321
1417
|
|
|
1418
|
+
#beginTerminal(session: DapSession): void {
|
|
1419
|
+
session.inflightTerminal++;
|
|
1420
|
+
if (session.inflightTerminal !== 1) return;
|
|
1421
|
+
session.terminalIdle = new Promise(resolve => {
|
|
1422
|
+
session.resolveTerminalIdle = resolve;
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
#endTerminal(session: DapSession): void {
|
|
1427
|
+
session.inflightTerminal = Math.max(0, session.inflightTerminal - 1);
|
|
1428
|
+
if (session.inflightTerminal !== 0) return;
|
|
1429
|
+
session.resolveTerminalIdle?.();
|
|
1430
|
+
session.resolveTerminalIdle = undefined;
|
|
1431
|
+
session.terminalIdle = undefined;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async #waitTerminalIdle(session: DapSession): Promise<void> {
|
|
1435
|
+
while (session.inflightTerminal > 0 && session.terminalIdle) {
|
|
1436
|
+
await session.terminalIdle;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1322
1440
|
async #disposeSession(session: DapSession) {
|
|
1441
|
+
session.closing = true;
|
|
1442
|
+
if (session.disposed) {
|
|
1443
|
+
await this.#waitTerminalIdle(session);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
session.disposed = true;
|
|
1447
|
+
await this.#waitTerminalIdle(session);
|
|
1323
1448
|
if (this.#activeSessionId === session.id) {
|
|
1324
1449
|
this.#activeSessionId = null;
|
|
1325
1450
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
3
|
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
@@ -180,10 +181,10 @@ export async function writeCrashReport(
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
try {
|
|
183
|
-
const
|
|
184
|
-
await ensurePrivateDiagnosticsDirectory(
|
|
184
|
+
const directory = diagnosticsDirectoryPath(getCrashDiagnosticsDirectory(options.env));
|
|
185
|
+
await ensurePrivateDiagnosticsDirectory(directory);
|
|
185
186
|
const filename = `${report.createdAt.replace(/[:.]/g, "-")}-${report.kind}-${report.class}-${process.pid}.json`;
|
|
186
|
-
const reportPath = path.join(
|
|
187
|
+
const reportPath = path.join(directory, filename);
|
|
187
188
|
await writePrivateCrashReport(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
188
189
|
return { report, path: reportPath, enabled };
|
|
189
190
|
} catch {
|
|
@@ -197,19 +198,100 @@ export function formatCrashDiagnosticNotice(result: CrashReportWriteResult): str
|
|
|
197
198
|
return `[crash:${result.report.kind}:${result.report.class}] ${result.report.reason}${location}`;
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
const POSIX_NOFOLLOW = process.platform === "win32" ? 0 : (fsConstants.O_NOFOLLOW ?? 0);
|
|
202
|
+
|
|
203
|
+
function isFilesystemRoot(dir: string): boolean {
|
|
204
|
+
const root = path.parse(dir).root;
|
|
205
|
+
return root.length > 0 && dir === root;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Drop trailing platform separators and "." segments. A backslash stays in the name on POSIX. ".." is unchanged. */
|
|
209
|
+
function diagnosticsDirectoryPath(dir: string): string {
|
|
210
|
+
const sep = path.sep;
|
|
211
|
+
let current = dir;
|
|
212
|
+
while (!isFilesystemRoot(current)) {
|
|
213
|
+
let end = current.length;
|
|
214
|
+
while (end > 0 && current[end - 1] === sep) end -= 1;
|
|
215
|
+
if (end !== current.length) {
|
|
216
|
+
const stripped = current.slice(0, end);
|
|
217
|
+
if (stripped === "" || isFilesystemRoot(stripped)) return path.parse(current).root || current;
|
|
218
|
+
if (process.platform === "win32" && /^[A-Za-z]:$/.test(stripped)) return `${stripped}\\`;
|
|
219
|
+
current = stripped;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const dotSuffix = `${sep}.`;
|
|
223
|
+
if (current.endsWith(dotSuffix) && !current.endsWith(`${sep}..`)) {
|
|
224
|
+
const parent = current.slice(0, -dotSuffix.length);
|
|
225
|
+
if (parent === "" || isFilesystemRoot(parent)) {
|
|
226
|
+
return parent === "" ? path.parse(current).root || current : parent;
|
|
227
|
+
}
|
|
228
|
+
if (process.platform === "win32" && /^[A-Za-z]:$/.test(parent)) return `${parent}\\`;
|
|
229
|
+
current = parent;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
return current;
|
|
233
|
+
}
|
|
234
|
+
return current;
|
|
235
|
+
}
|
|
236
|
+
|
|
200
237
|
async function ensurePrivateDiagnosticsDirectory(dir: string): Promise<void> {
|
|
238
|
+
await assertRealDiagnosticsDirectory(dir);
|
|
201
239
|
await fs.mkdir(dir, { recursive: true, mode: DIRECTORY_MODE });
|
|
240
|
+
await assertRealDiagnosticsDirectory(dir);
|
|
241
|
+
await chmodRealDiagnosticsDirectory(dir);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function assertRealDiagnosticsDirectory(dir: string): Promise<void> {
|
|
245
|
+
let stat: Awaited<ReturnType<typeof fs.lstat>>;
|
|
246
|
+
try {
|
|
247
|
+
stat = await fs.lstat(dir);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
253
|
+
throw new Error("crash diagnostics path is not a private directory");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function chmodRealDiagnosticsDirectory(dir: string): Promise<void> {
|
|
258
|
+
const stat = await fs.lstat(dir);
|
|
259
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
260
|
+
throw new Error("crash diagnostics path is not a private directory");
|
|
261
|
+
}
|
|
262
|
+
// macOS lchmod sets the mode without read permission and without following a symlink.
|
|
263
|
+
// Linux has no equivalent here, so an unreadable directory is left unchanged. A path chmod would follow a swapped symlink.
|
|
264
|
+
if (process.platform === "darwin" && typeof fs.lchmod === "function") {
|
|
265
|
+
await fs.lchmod(dir, DIRECTORY_MODE);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const directoryFlag = fsConstants.O_DIRECTORY ?? 0;
|
|
269
|
+
const pathFlag = (fsConstants as { O_PATH?: number }).O_PATH ?? 0;
|
|
270
|
+
if (POSIX_NOFOLLOW !== 0 && directoryFlag !== 0) {
|
|
271
|
+
const openFlag = pathFlag !== 0 ? pathFlag : fsConstants.O_RDONLY;
|
|
272
|
+
const handle = await fs.open(dir, openFlag | directoryFlag | POSIX_NOFOLLOW);
|
|
273
|
+
try {
|
|
274
|
+
await handle.chmod(DIRECTORY_MODE);
|
|
275
|
+
} finally {
|
|
276
|
+
await handle.close();
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
202
280
|
await fs.chmod(dir, DIRECTORY_MODE);
|
|
203
281
|
}
|
|
204
282
|
|
|
205
283
|
async function writePrivateCrashReport(reportPath: string, contents: string): Promise<void> {
|
|
206
|
-
const file = await fs.open(
|
|
284
|
+
const file = await fs.open(
|
|
285
|
+
reportPath,
|
|
286
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | POSIX_NOFOLLOW,
|
|
287
|
+
REPORT_FILE_MODE,
|
|
288
|
+
);
|
|
207
289
|
try {
|
|
208
290
|
await file.writeFile(contents);
|
|
291
|
+
await file.chmod(REPORT_FILE_MODE);
|
|
209
292
|
} finally {
|
|
210
293
|
await file.close();
|
|
211
294
|
}
|
|
212
|
-
await fs.chmod(reportPath, REPORT_FILE_MODE);
|
|
213
295
|
}
|
|
214
296
|
|
|
215
297
|
function stringifyError(error: unknown): string {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Creates a .tar.gz archive with session data, logs, system info, and optional profiling data.
|
|
5
5
|
*/
|
|
6
|
+
import { constants as fsConstants } from "node:fs";
|
|
6
7
|
import * as fs from "node:fs/promises";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
import type { WorkProfile } from "@gajae-code/natives";
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
getLogPath,
|
|
14
15
|
getLogsDir,
|
|
15
16
|
getReportsDir,
|
|
17
|
+
hasFsCode,
|
|
16
18
|
isEnoent,
|
|
17
19
|
redactCrashSecrets,
|
|
18
20
|
resolveEquivalentPath,
|
|
@@ -133,6 +135,72 @@ export interface DebugLogSource {
|
|
|
133
135
|
loadOlderLogs(limitDays?: number): Promise<string>;
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/** Owner-only mode. umask cannot widen this, and fchmod repeats it after open. */
|
|
139
|
+
const REPORT_BUNDLE_MODE = 0o600;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Open the archive path without following a final symlink.
|
|
143
|
+
* A missing path is created with O_EXCL. An existing regular file is replaced.
|
|
144
|
+
* A symlink fails the open (ELOOP, or EEXIST on macOS with O_EXCL) and is left in place.
|
|
145
|
+
*/
|
|
146
|
+
async function openReportBundleForWrite(outputPath: string): Promise<fs.FileHandle> {
|
|
147
|
+
const noFollow = process.platform === "win32" ? 0 : (fsConstants.O_NOFOLLOW ?? 0);
|
|
148
|
+
const createFlags = fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow;
|
|
149
|
+
const replaceFlags = fsConstants.O_WRONLY | noFollow;
|
|
150
|
+
|
|
151
|
+
const openRegular = async (): Promise<fs.FileHandle> => {
|
|
152
|
+
const current = await fs.lstat(outputPath);
|
|
153
|
+
if (current.isSymbolicLink() || !current.isFile()) {
|
|
154
|
+
throw new Error("report bundle output is not a regular file");
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
return await fs.open(outputPath, replaceFlags, REPORT_BUNDLE_MODE);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (hasFsCode(error, "ELOOP")) throw new Error("report bundle output is not a regular file");
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
let missing = false;
|
|
165
|
+
try {
|
|
166
|
+
await fs.lstat(outputPath);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (!isEnoent(error)) throw error;
|
|
169
|
+
missing = true;
|
|
170
|
+
}
|
|
171
|
+
if (!missing) return openRegular();
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
return await fs.open(outputPath, createFlags, REPORT_BUNDLE_MODE);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (hasFsCode(error, "ELOOP")) throw new Error("report bundle output is not a regular file");
|
|
177
|
+
if (!hasFsCode(error, "EEXIST")) throw error;
|
|
178
|
+
}
|
|
179
|
+
return openRegular();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function writeReportBundleFile(outputPath: string, bytes: Uint8Array): Promise<void> {
|
|
183
|
+
const handle = await openReportBundleForWrite(outputPath);
|
|
184
|
+
try {
|
|
185
|
+
const opened = await handle.stat();
|
|
186
|
+
const current = await fs.lstat(outputPath);
|
|
187
|
+
if (
|
|
188
|
+
current.isSymbolicLink() ||
|
|
189
|
+
!opened.isFile() ||
|
|
190
|
+
!current.isFile() ||
|
|
191
|
+
opened.dev !== current.dev ||
|
|
192
|
+
opened.ino !== current.ino
|
|
193
|
+
) {
|
|
194
|
+
throw new Error("report bundle output is not a regular file");
|
|
195
|
+
}
|
|
196
|
+
if (process.platform !== "win32") await handle.chmod(REPORT_BUNDLE_MODE);
|
|
197
|
+
await handle.truncate(0);
|
|
198
|
+
await handle.writeFile(bytes);
|
|
199
|
+
} finally {
|
|
200
|
+
await handle.close();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
136
204
|
/**
|
|
137
205
|
* Create a debug report bundle.
|
|
138
206
|
*
|
|
@@ -237,8 +305,9 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
|
|
|
237
305
|
data[archivePath] = redactCrashSecrets(content);
|
|
238
306
|
}
|
|
239
307
|
|
|
240
|
-
// Write archive
|
|
241
|
-
await Bun.Archive
|
|
308
|
+
// Write archive. Bytes are built in memory so the path open can refuse a symlink.
|
|
309
|
+
const archiveBytes = await new Bun.Archive(data, { compress: "gzip" }).bytes();
|
|
310
|
+
await writeReportBundleFile(outputPath, archiveBytes);
|
|
242
311
|
|
|
243
312
|
return { path: outputPath, files };
|
|
244
313
|
}
|
package/src/discovery/builtin.ts
CHANGED
|
@@ -399,10 +399,11 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
|
|
|
399
399
|
const commandsDir = path.join(dir, "commands");
|
|
400
400
|
const result = await loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, level, {
|
|
401
401
|
extensions: ["md"],
|
|
402
|
+
recursive: true,
|
|
402
403
|
scope: readScopeForLevel(level),
|
|
403
|
-
transform: (
|
|
404
|
-
name:
|
|
405
|
-
path,
|
|
404
|
+
transform: (_name, content, filePath, source) => ({
|
|
405
|
+
name: path.relative(commandsDir, filePath).replace(/\.md$/, "").split(path.sep).join(":"),
|
|
406
|
+
path: filePath,
|
|
406
407
|
content,
|
|
407
408
|
level,
|
|
408
409
|
_source: source,
|