@nogataka/smart-edit 0.0.14
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 +22 -0
- package/README.md +244 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +7 -0
- package/dist/devtools/generate_prompt_factory.d.ts +5 -0
- package/dist/devtools/generate_prompt_factory.js +114 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +34 -0
- package/dist/interprompt/index.d.ts +2 -0
- package/dist/interprompt/index.js +1 -0
- package/dist/interprompt/jinja_template.d.ts +10 -0
- package/dist/interprompt/jinja_template.js +174 -0
- package/dist/interprompt/multilang_prompt.d.ts +54 -0
- package/dist/interprompt/multilang_prompt.js +302 -0
- package/dist/interprompt/prompt_factory.d.ts +16 -0
- package/dist/interprompt/prompt_factory.js +189 -0
- package/dist/interprompt/util/class_decorators.d.ts +1 -0
- package/dist/interprompt/util/class_decorators.js +1 -0
- package/dist/interprompt/util/index.d.ts +1 -0
- package/dist/interprompt/util/index.js +1 -0
- package/dist/serena/agent.d.ts +118 -0
- package/dist/serena/agent.js +675 -0
- package/dist/serena/agno.d.ts +111 -0
- package/dist/serena/agno.js +278 -0
- package/dist/serena/analytics.d.ts +24 -0
- package/dist/serena/analytics.js +119 -0
- package/dist/serena/cli.d.ts +9 -0
- package/dist/serena/cli.js +731 -0
- package/dist/serena/code_editor.d.ts +42 -0
- package/dist/serena/code_editor.js +239 -0
- package/dist/serena/config/context_mode.d.ts +41 -0
- package/dist/serena/config/context_mode.js +239 -0
- package/dist/serena/config/serena_config.d.ts +134 -0
- package/dist/serena/config/serena_config.js +718 -0
- package/dist/serena/constants.d.ts +18 -0
- package/dist/serena/constants.js +27 -0
- package/dist/serena/dashboard.d.ts +55 -0
- package/dist/serena/dashboard.js +472 -0
- package/dist/serena/generated/generated_prompt_factory.d.ts +27 -0
- package/dist/serena/generated/generated_prompt_factory.js +42 -0
- package/dist/serena/gui_log_viewer.d.ts +41 -0
- package/dist/serena/gui_log_viewer.js +436 -0
- package/dist/serena/mcp.d.ts +118 -0
- package/dist/serena/mcp.js +904 -0
- package/dist/serena/project.d.ts +62 -0
- package/dist/serena/project.js +321 -0
- package/dist/serena/prompt_factory.d.ts +20 -0
- package/dist/serena/prompt_factory.js +42 -0
- package/dist/serena/resources/config/contexts/agent.yml +8 -0
- package/dist/serena/resources/config/contexts/chatgpt.yml +28 -0
- package/dist/serena/resources/config/contexts/codex.yml +27 -0
- package/dist/serena/resources/config/contexts/context.template.yml +11 -0
- package/dist/serena/resources/config/contexts/desktop-app.yml +17 -0
- package/dist/serena/resources/config/contexts/ide-assistant.yml +26 -0
- package/dist/serena/resources/config/contexts/oaicompat-agent.yml +8 -0
- package/dist/serena/resources/config/internal_modes/jetbrains.yml +15 -0
- package/dist/serena/resources/config/modes/editing.yml +112 -0
- package/dist/serena/resources/config/modes/interactive.yml +11 -0
- package/dist/serena/resources/config/modes/mode.template.yml +7 -0
- package/dist/serena/resources/config/modes/no-onboarding.yml +8 -0
- package/dist/serena/resources/config/modes/onboarding.yml +16 -0
- package/dist/serena/resources/config/modes/one-shot.yml +15 -0
- package/dist/serena/resources/config/modes/planning.yml +15 -0
- package/dist/serena/resources/config/prompt_templates/simple_tool_outputs.yml +75 -0
- package/dist/serena/resources/config/prompt_templates/system_prompt.yml +66 -0
- package/dist/serena/resources/dashboard/dashboard.js +815 -0
- package/dist/serena/resources/dashboard/index.html +314 -0
- package/dist/serena/resources/dashboard/jquery.min.js +3 -0
- package/dist/serena/resources/dashboard/serena-icon-16.png +0 -0
- package/dist/serena/resources/dashboard/serena-icon-32.png +0 -0
- package/dist/serena/resources/dashboard/serena-icon-48.png +0 -0
- package/dist/serena/resources/dashboard/serena-logs-dark-mode.png +0 -0
- package/dist/serena/resources/dashboard/serena-logs.png +0 -0
- package/dist/serena/resources/project.template.yml +67 -0
- package/dist/serena/resources/serena_config.template.yml +85 -0
- package/dist/serena/symbol.d.ts +199 -0
- package/dist/serena/symbol.js +616 -0
- package/dist/serena/text_utils.d.ts +51 -0
- package/dist/serena/text_utils.js +267 -0
- package/dist/serena/tools/cmd_tools.d.ts +31 -0
- package/dist/serena/tools/cmd_tools.js +48 -0
- package/dist/serena/tools/config_tools.d.ts +53 -0
- package/dist/serena/tools/config_tools.js +176 -0
- package/dist/serena/tools/file_tools.d.ts +231 -0
- package/dist/serena/tools/file_tools.js +511 -0
- package/dist/serena/tools/index.d.ts +7 -0
- package/dist/serena/tools/index.js +7 -0
- package/dist/serena/tools/memory_tools.d.ts +60 -0
- package/dist/serena/tools/memory_tools.js +135 -0
- package/dist/serena/tools/symbol_tools.d.ts +165 -0
- package/dist/serena/tools/symbol_tools.js +362 -0
- package/dist/serena/tools/tools_base.d.ts +162 -0
- package/dist/serena/tools/tools_base.js +378 -0
- package/dist/serena/tools/workflow_tools.d.ts +35 -0
- package/dist/serena/tools/workflow_tools.js +161 -0
- package/dist/serena/util/class_decorators.d.ts +7 -0
- package/dist/serena/util/class_decorators.js +37 -0
- package/dist/serena/util/exception.d.ts +8 -0
- package/dist/serena/util/exception.js +53 -0
- package/dist/serena/util/file_system.d.ts +30 -0
- package/dist/serena/util/file_system.js +352 -0
- package/dist/serena/util/general.d.ts +11 -0
- package/dist/serena/util/general.js +42 -0
- package/dist/serena/util/git.d.ts +11 -0
- package/dist/serena/util/git.js +37 -0
- package/dist/serena/util/inspection.d.ts +45 -0
- package/dist/serena/util/inspection.js +221 -0
- package/dist/serena/util/logging.d.ts +46 -0
- package/dist/serena/util/logging.js +205 -0
- package/dist/serena/util/shell.d.ts +21 -0
- package/dist/serena/util/shell.js +95 -0
- package/dist/serena/util/thread.d.ts +23 -0
- package/dist/serena/util/thread.js +88 -0
- package/dist/serena/version.d.ts +1 -0
- package/dist/serena/version.js +23 -0
- package/dist/solidlsp/language_servers/autoload.d.ts +23 -0
- package/dist/solidlsp/language_servers/autoload.js +25 -0
- package/dist/solidlsp/language_servers/bash_language_server.d.ts +10 -0
- package/dist/solidlsp/language_servers/bash_language_server.js +64 -0
- package/dist/solidlsp/language_servers/clangd_language_server.d.ts +13 -0
- package/dist/solidlsp/language_servers/clangd_language_server.js +110 -0
- package/dist/solidlsp/language_servers/clojure_lsp.d.ts +13 -0
- package/dist/solidlsp/language_servers/clojure_lsp.js +137 -0
- package/dist/solidlsp/language_servers/common.d.ts +41 -0
- package/dist/solidlsp/language_servers/common.js +365 -0
- package/dist/solidlsp/language_servers/csharp_language_server.d.ts +21 -0
- package/dist/solidlsp/language_servers/csharp_language_server.js +694 -0
- package/dist/solidlsp/language_servers/dart_language_server.d.ts +10 -0
- package/dist/solidlsp/language_servers/dart_language_server.js +122 -0
- package/dist/solidlsp/language_servers/eclipse_jdtls.d.ts +24 -0
- package/dist/solidlsp/language_servers/eclipse_jdtls.js +671 -0
- package/dist/solidlsp/language_servers/erlang_language_server.d.ts +22 -0
- package/dist/solidlsp/language_servers/erlang_language_server.js +327 -0
- package/dist/solidlsp/language_servers/gopls.d.ts +12 -0
- package/dist/solidlsp/language_servers/gopls.js +59 -0
- package/dist/solidlsp/language_servers/intelephense.d.ts +13 -0
- package/dist/solidlsp/language_servers/intelephense.js +121 -0
- package/dist/solidlsp/language_servers/jedi_server.d.ts +18 -0
- package/dist/solidlsp/language_servers/jedi_server.js +234 -0
- package/dist/solidlsp/language_servers/kotlin_language_server.d.ts +19 -0
- package/dist/solidlsp/language_servers/kotlin_language_server.js +474 -0
- package/dist/solidlsp/language_servers/lua_ls.d.ts +18 -0
- package/dist/solidlsp/language_servers/lua_ls.js +319 -0
- package/dist/solidlsp/language_servers/nixd_language_server.d.ts +17 -0
- package/dist/solidlsp/language_servers/nixd_language_server.js +341 -0
- package/dist/solidlsp/language_servers/pyright_server.d.ts +19 -0
- package/dist/solidlsp/language_servers/pyright_server.js +180 -0
- package/dist/solidlsp/language_servers/r_language_server.d.ts +19 -0
- package/dist/solidlsp/language_servers/r_language_server.js +184 -0
- package/dist/solidlsp/language_servers/ruby_common.d.ts +10 -0
- package/dist/solidlsp/language_servers/ruby_common.js +136 -0
- package/dist/solidlsp/language_servers/ruby_lsp.d.ts +18 -0
- package/dist/solidlsp/language_servers/ruby_lsp.js +230 -0
- package/dist/solidlsp/language_servers/rust_analyzer.d.ts +13 -0
- package/dist/solidlsp/language_servers/rust_analyzer.js +96 -0
- package/dist/solidlsp/language_servers/solargraph.d.ts +18 -0
- package/dist/solidlsp/language_servers/solargraph.js +208 -0
- package/dist/solidlsp/language_servers/sourcekit_lsp.d.ts +24 -0
- package/dist/solidlsp/language_servers/sourcekit_lsp.js +449 -0
- package/dist/solidlsp/language_servers/terraform_ls.d.ts +13 -0
- package/dist/solidlsp/language_servers/terraform_ls.js +139 -0
- package/dist/solidlsp/language_servers/typescript_language_server.d.ts +20 -0
- package/dist/solidlsp/language_servers/typescript_language_server.js +237 -0
- package/dist/solidlsp/language_servers/vts_language_server.d.ts +13 -0
- package/dist/solidlsp/language_servers/vts_language_server.js +121 -0
- package/dist/solidlsp/language_servers/zls.d.ts +20 -0
- package/dist/solidlsp/language_servers/zls.js +254 -0
- package/dist/solidlsp/ls.d.ts +197 -0
- package/dist/solidlsp/ls.js +507 -0
- package/dist/solidlsp/ls_config.d.ts +43 -0
- package/dist/solidlsp/ls_config.js +157 -0
- package/dist/solidlsp/ls_exceptions.d.ts +5 -0
- package/dist/solidlsp/ls_exceptions.js +14 -0
- package/dist/solidlsp/ls_handler.d.ts +54 -0
- package/dist/solidlsp/ls_handler.js +406 -0
- package/dist/solidlsp/ls_request.d.ts +31 -0
- package/dist/solidlsp/ls_request.js +42 -0
- package/dist/solidlsp/ls_types.d.ts +7 -0
- package/dist/solidlsp/ls_types.js +8 -0
- package/dist/solidlsp/lsp_protocol_handler/server.d.ts +61 -0
- package/dist/solidlsp/lsp_protocol_handler/server.js +68 -0
- package/dist/solidlsp/util/subprocess_util.d.ts +6 -0
- package/dist/solidlsp/util/subprocess_util.js +11 -0
- package/dist/solidlsp/util/zip.d.ts +25 -0
- package/dist/solidlsp/util/zip.js +188 -0
- package/package.json +65 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional } from './tools_base.js';
|
|
3
|
+
import { ListMemoriesTool } from './memory_tools.js';
|
|
4
|
+
function ensureString(value) {
|
|
5
|
+
if (typeof value === 'string') {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
if (value === undefined || value === null) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
12
|
+
return value.toString();
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === 'symbol') {
|
|
15
|
+
return value.toString();
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === 'function') {
|
|
18
|
+
return '[function]';
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'object') {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return Object.prototype.toString.call(value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
async function callPromptFactoryMethod(promptFactory, methodCandidates, ...args) {
|
|
31
|
+
for (const name of methodCandidates) {
|
|
32
|
+
const candidate = Reflect.get(promptFactory, name);
|
|
33
|
+
if (typeof candidate === 'function') {
|
|
34
|
+
const result = await Promise.resolve(candidate.apply(promptFactory, args));
|
|
35
|
+
return ensureString(result);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Prompt factory does not implement expected method: ${methodCandidates.join(' or ')}`);
|
|
39
|
+
}
|
|
40
|
+
function normalizeSystemName(platformName) {
|
|
41
|
+
switch (platformName) {
|
|
42
|
+
case 'win32':
|
|
43
|
+
return 'Windows';
|
|
44
|
+
case 'darwin':
|
|
45
|
+
return 'Darwin';
|
|
46
|
+
case 'linux':
|
|
47
|
+
return 'Linux';
|
|
48
|
+
default:
|
|
49
|
+
if (platformName.length === 0) {
|
|
50
|
+
return 'Unknown';
|
|
51
|
+
}
|
|
52
|
+
return platformName.charAt(0).toUpperCase() + platformName.slice(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getToolInstance(agent, toolClass) {
|
|
56
|
+
const lookupAgent = agent;
|
|
57
|
+
if (typeof lookupAgent.getTool === 'function') {
|
|
58
|
+
return lookupAgent.getTool(toolClass);
|
|
59
|
+
}
|
|
60
|
+
if (typeof lookupAgent.get_tool === 'function') {
|
|
61
|
+
return lookupAgent.get_tool(toolClass);
|
|
62
|
+
}
|
|
63
|
+
const FallbackCtor = toolClass;
|
|
64
|
+
return new FallbackCtor(agent);
|
|
65
|
+
}
|
|
66
|
+
async function callAgentSystemPrompt(agent) {
|
|
67
|
+
const candidate = agent;
|
|
68
|
+
if (typeof candidate.createSystemPrompt === 'function') {
|
|
69
|
+
const result = await Promise.resolve(candidate.createSystemPrompt());
|
|
70
|
+
return ensureString(result);
|
|
71
|
+
}
|
|
72
|
+
if (typeof candidate.create_system_prompt === 'function') {
|
|
73
|
+
const result = await Promise.resolve(candidate.create_system_prompt());
|
|
74
|
+
return ensureString(result);
|
|
75
|
+
}
|
|
76
|
+
throw new Error('Agent does not implement a system prompt creation method.');
|
|
77
|
+
}
|
|
78
|
+
export class CheckOnboardingPerformedTool extends Tool {
|
|
79
|
+
static description = 'Checks whether project onboarding was already performed.';
|
|
80
|
+
async apply(_args = {}) {
|
|
81
|
+
const listTool = getToolInstance(this.agent, ListMemoriesTool);
|
|
82
|
+
const raw = await Promise.resolve(listTool.apply());
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(ensureString(raw));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
+
throw new Error(`Unable to parse ListMemoriesTool output as JSON: ${message}`);
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(parsed)) {
|
|
92
|
+
throw new Error('ListMemoriesTool returned non-array JSON output.');
|
|
93
|
+
}
|
|
94
|
+
if (parsed.length === 0) {
|
|
95
|
+
return ('Onboarding not performed yet (no memories available). ' +
|
|
96
|
+
'You should perform onboarding by calling the `onboarding` tool before proceeding with the task.');
|
|
97
|
+
}
|
|
98
|
+
const lines = [
|
|
99
|
+
'The onboarding was already performed, below is the list of available memories.',
|
|
100
|
+
'Do not read them immediately, just remember that they exist and that you can read them later, if it is necessary',
|
|
101
|
+
'for the current task.',
|
|
102
|
+
'Some memories may be based on previous conversations, others may be general for the current project.',
|
|
103
|
+
'You should be able to tell which one you need based on the name of the memory.',
|
|
104
|
+
'',
|
|
105
|
+
JSON.stringify(parsed)
|
|
106
|
+
];
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export class OnboardingTool extends Tool {
|
|
111
|
+
static description = 'Provides onboarding instructions (project structure, essential tasks, etc.) when onboarding has not been performed.';
|
|
112
|
+
async apply(_args = {}) {
|
|
113
|
+
const system = normalizeSystemName(os.platform());
|
|
114
|
+
const result = await callPromptFactoryMethod(this.promptFactory, ['create_onboarding_prompt', 'createOnboardingPrompt'], { system });
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export class ThinkAboutCollectedInformationTool extends Tool {
|
|
119
|
+
static description = 'Encourages the agent to reflect on whether the gathered information is sufficient and relevant.';
|
|
120
|
+
async apply(_args = {}) {
|
|
121
|
+
const result = await callPromptFactoryMethod(this.promptFactory, ['create_think_about_collected_information', 'createThinkAboutCollectedInformation']);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export class ThinkAboutTaskAdherenceTool extends Tool {
|
|
126
|
+
static description = 'Guides the agent to confirm alignment with the current task before making code changes.';
|
|
127
|
+
async apply(_args = {}) {
|
|
128
|
+
const result = await callPromptFactoryMethod(this.promptFactory, ['create_think_about_task_adherence', 'createThinkAboutTaskAdherence']);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export class ThinkAboutWhetherYouAreDoneTool extends Tool {
|
|
133
|
+
static description = 'Helps determine whether the requested task is fully completed.';
|
|
134
|
+
async apply(_args = {}) {
|
|
135
|
+
const result = await callPromptFactoryMethod(this.promptFactory, ['create_think_about_whether_you_are_done', 'createThinkAboutWhetherYouAreDone']);
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export class SummarizeChangesTool extends Tool {
|
|
140
|
+
static markers = new Set([ToolMarkerOptional]);
|
|
141
|
+
static description = 'Provides guidelines for summarizing codebase changes after completing a task.';
|
|
142
|
+
async apply(_args = {}) {
|
|
143
|
+
const result = await callPromptFactoryMethod(this.promptFactory, ['create_summarize_changes', 'createSummarizeChanges']);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export class PrepareForNewConversationTool extends Tool {
|
|
148
|
+
static description = 'Provides instructions to prepare for continuing work in a new conversation context.';
|
|
149
|
+
async apply(_args = {}) {
|
|
150
|
+
const result = await callPromptFactoryMethod(this.promptFactory, ['create_prepare_for_new_conversation', 'createPrepareForNewConversation']);
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export class InitialInstructionsTool extends Tool {
|
|
155
|
+
static markers = new Set([ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional]);
|
|
156
|
+
static description = 'Returns the initial system instructions for the current project when they cannot be provided via the system prompt.';
|
|
157
|
+
async apply(_args = {}) {
|
|
158
|
+
const result = await callAgentSystemPrompt(this.agent);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type ClassConstructor<T extends object = object> = abstract new (...args: unknown[]) => T;
|
|
2
|
+
interface DecoratorContextLike {
|
|
3
|
+
kind?: string;
|
|
4
|
+
name?: string | symbol;
|
|
5
|
+
}
|
|
6
|
+
export declare function singleton<T extends ClassConstructor>(constructor: T, context?: DecoratorContextLike): T;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function singleton(constructor, context) {
|
|
2
|
+
return wrapAsSingleton(constructor, context);
|
|
3
|
+
}
|
|
4
|
+
function wrapAsSingleton(constructor, context) {
|
|
5
|
+
let instance;
|
|
6
|
+
let proxy;
|
|
7
|
+
const handler = {
|
|
8
|
+
construct(target, args, newTarget) {
|
|
9
|
+
if (instance && (newTarget === proxy || newTarget === target)) {
|
|
10
|
+
return instance;
|
|
11
|
+
}
|
|
12
|
+
const actualTarget = newTarget === proxy ? target : newTarget;
|
|
13
|
+
instance = Reflect.construct(target, args, actualTarget);
|
|
14
|
+
return instance;
|
|
15
|
+
},
|
|
16
|
+
apply(target, thisArg, args) {
|
|
17
|
+
instance ??= Reflect.construct(target, args, target);
|
|
18
|
+
return instance;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
proxy = new Proxy(constructor, handler);
|
|
22
|
+
const desiredName = typeof context?.name === 'string' && context.kind === 'class'
|
|
23
|
+
? context.name
|
|
24
|
+
: constructor.name;
|
|
25
|
+
if (desiredName && desiredName !== proxy.name) {
|
|
26
|
+
try {
|
|
27
|
+
Object.defineProperty(proxy, 'name', {
|
|
28
|
+
value: desiredName,
|
|
29
|
+
configurable: true
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// プロキシ化したコンストラクタでは name を再定義できない場合があるが、機能に影響はないため無視する
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return proxy;
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SerenaLogger } from './logging.js';
|
|
2
|
+
export interface ShowFatalExceptionOptions {
|
|
3
|
+
logger?: SerenaLogger;
|
|
4
|
+
printToStderr?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function evaluateHeadlessEnvironment(env: NodeJS.ProcessEnv, platform: NodeJS.Platform, release: string, fileExists: (path: string) => boolean): boolean;
|
|
7
|
+
export declare function isHeadlessEnvironment(): boolean;
|
|
8
|
+
export declare function showFatalExceptionSafe(error: unknown, { logger, printToStderr }?: ShowFatalExceptionOptions): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { createSerenaLogger } from './logging.js';
|
|
4
|
+
const { logger: consoleLogger } = createSerenaLogger({ level: 'debug' });
|
|
5
|
+
export function evaluateHeadlessEnvironment(env, platform, release, fileExists) {
|
|
6
|
+
if (platform === 'win32') {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
if (!env.DISPLAY) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
if (env.SSH_CONNECTION || env.SSH_CLIENT) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (env.CI || env.CONTAINER || fileExists('/.dockerenv')) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (release.toLowerCase().includes('microsoft')) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
export function isHeadlessEnvironment() {
|
|
24
|
+
return evaluateHeadlessEnvironment(process.env, process.platform, os.release(), existsSync);
|
|
25
|
+
}
|
|
26
|
+
export async function showFatalExceptionSafe(error, { logger = consoleLogger, printToStderr = true } = {}) {
|
|
27
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
28
|
+
logger.error(`Fatal exception: ${errorMessage}`, error);
|
|
29
|
+
if (printToStderr) {
|
|
30
|
+
if (error instanceof Error && error.stack) {
|
|
31
|
+
console.error(`Fatal exception: ${error.message}\n${error.stack}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.error(`Fatal exception: ${errorMessage}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (isHeadlessEnvironment()) {
|
|
38
|
+
logger.debug('Skipping GUI error display in headless environment');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const { showFatalException } = await import('../gui_log_viewer.js');
|
|
43
|
+
if (typeof showFatalException === 'function') {
|
|
44
|
+
await Promise.resolve(showFatalException(error));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
logger.debug('GUI error display is unavailable: showFatalException not implemented');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (guiError) {
|
|
51
|
+
logger.debug('Failed to show GUI error dialog', guiError);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface ScanResult {
|
|
2
|
+
directories: string[];
|
|
3
|
+
files: string[];
|
|
4
|
+
}
|
|
5
|
+
export type IgnorePredicate = (absolutePath: string) => boolean;
|
|
6
|
+
export declare function scanDirectory(targetPath: string, recursive?: boolean, relativeTo?: string, isIgnoredDir?: IgnorePredicate, isIgnoredFile?: IgnorePredicate): ScanResult;
|
|
7
|
+
export declare function findAllNonIgnoredFiles(repoRoot: string): string[];
|
|
8
|
+
export declare class GitignoreSpec {
|
|
9
|
+
readonly filePath: string;
|
|
10
|
+
readonly patterns: string[];
|
|
11
|
+
private readonly matcher;
|
|
12
|
+
constructor(filePath: string, patterns: string[]);
|
|
13
|
+
matches(relativePath: string): boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare class GitignoreParser {
|
|
16
|
+
readonly repoRoot: string;
|
|
17
|
+
private ignoreSpecs;
|
|
18
|
+
constructor(repoRoot: string);
|
|
19
|
+
shouldIgnore(inputPath: string): boolean;
|
|
20
|
+
getIgnoreSpecs(): GitignoreSpec[];
|
|
21
|
+
reload(): void;
|
|
22
|
+
private loadGitignoreFiles;
|
|
23
|
+
private iterGitignoreFiles;
|
|
24
|
+
private createIgnoreSpec;
|
|
25
|
+
private parseGitignoreContent;
|
|
26
|
+
}
|
|
27
|
+
export interface PathMatcher {
|
|
28
|
+
ignores(path: string): boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function matchPath(relativePath: string, matcher: PathMatcher, rootPath?: string): boolean;
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { performance } from 'node:perf_hooks';
|
|
4
|
+
import ignore from 'ignore';
|
|
5
|
+
import { createSerenaLogger } from './logging.js';
|
|
6
|
+
const { logger: fileSystemLogger } = createSerenaLogger({
|
|
7
|
+
level: 'debug',
|
|
8
|
+
emitToConsole: false,
|
|
9
|
+
name: 'SerenaFileSystem'
|
|
10
|
+
});
|
|
11
|
+
export function scanDirectory(targetPath, recursive = false, relativeTo, isIgnoredDir, isIgnoredFile) {
|
|
12
|
+
const ignoreDir = isIgnoredDir ?? (() => false);
|
|
13
|
+
const ignoreFile = isIgnoredFile ?? (() => false);
|
|
14
|
+
const files = [];
|
|
15
|
+
const directories = [];
|
|
16
|
+
const absolutePath = path.resolve(targetPath);
|
|
17
|
+
const relativeBase = relativeTo ? path.resolve(relativeTo) : undefined;
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = fs.readdirSync(absolutePath, { withFileTypes: true });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (isPermissionError(error)) {
|
|
24
|
+
fileSystemLogger.debug('Skipping directory due to permission error', {
|
|
25
|
+
path: absolutePath,
|
|
26
|
+
error
|
|
27
|
+
});
|
|
28
|
+
return { directories: [], files: [] };
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const entryPath = path.join(absolutePath, entry.name);
|
|
34
|
+
const resultPath = relativeBase ? path.relative(relativeBase, entryPath) : entryPath;
|
|
35
|
+
let stats;
|
|
36
|
+
try {
|
|
37
|
+
stats = fs.statSync(entryPath);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (isPermissionError(error)) {
|
|
41
|
+
fileSystemLogger.debug('Skipping entry due to permission error', {
|
|
42
|
+
path: entryPath,
|
|
43
|
+
error
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
if (stats.isFile()) {
|
|
50
|
+
if (!ignoreFile(entryPath)) {
|
|
51
|
+
files.push(resultPath);
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (stats.isDirectory()) {
|
|
56
|
+
if (!ignoreDir(entryPath)) {
|
|
57
|
+
directories.push(resultPath);
|
|
58
|
+
if (recursive) {
|
|
59
|
+
const subResult = scanDirectory(entryPath, true, relativeTo, ignoreDir, ignoreFile);
|
|
60
|
+
files.push(...subResult.files);
|
|
61
|
+
directories.push(...subResult.directories);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { directories, files };
|
|
68
|
+
}
|
|
69
|
+
export function findAllNonIgnoredFiles(repoRoot) {
|
|
70
|
+
const parser = new GitignoreParser(repoRoot);
|
|
71
|
+
const { files } = scanDirectory(repoRoot, true, undefined, parser.shouldIgnore.bind(parser), parser.shouldIgnore.bind(parser));
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
export class GitignoreSpec {
|
|
75
|
+
filePath;
|
|
76
|
+
patterns;
|
|
77
|
+
matcher;
|
|
78
|
+
constructor(filePath, patterns) {
|
|
79
|
+
this.filePath = filePath;
|
|
80
|
+
this.patterns = patterns;
|
|
81
|
+
this.matcher = ignore();
|
|
82
|
+
if (patterns.length > 0) {
|
|
83
|
+
this.matcher.add(patterns);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
matches(relativePath) {
|
|
87
|
+
if (!this.patterns.length) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const candidate = normalizeCandidate(relativePath);
|
|
91
|
+
if (candidate === '') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const withoutLeadingSlash = candidate.startsWith('/') ? candidate.slice(1) : candidate;
|
|
95
|
+
if (this.matcher.ignores(withoutLeadingSlash)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (withoutLeadingSlash.endsWith('/')) {
|
|
99
|
+
const withoutTrailing = withoutLeadingSlash.slice(0, -1);
|
|
100
|
+
if (withoutTrailing && this.matcher.ignores(withoutTrailing)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const withTrailing = `${withoutLeadingSlash}/`;
|
|
106
|
+
if (this.matcher.ignores(withTrailing)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export class GitignoreParser {
|
|
114
|
+
repoRoot;
|
|
115
|
+
ignoreSpecs = [];
|
|
116
|
+
constructor(repoRoot) {
|
|
117
|
+
this.repoRoot = path.resolve(repoRoot);
|
|
118
|
+
this.loadGitignoreFiles();
|
|
119
|
+
}
|
|
120
|
+
shouldIgnore(inputPath) {
|
|
121
|
+
const absolutePath = path.isAbsolute(inputPath)
|
|
122
|
+
? path.resolve(inputPath)
|
|
123
|
+
: path.resolve(this.repoRoot, inputPath);
|
|
124
|
+
if (!isPathInsideRoot(absolutePath, this.repoRoot)) {
|
|
125
|
+
fileSystemLogger.debug('Ignoring path outside repository root', { path: absolutePath });
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
let relativePath = path.relative(this.repoRoot, absolutePath);
|
|
129
|
+
if (!relativePath) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const firstSegment = relativePath.split(path.sep)[0];
|
|
133
|
+
if (firstSegment === '.git') {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
const isDirectory = isDirectorySafe(absolutePath);
|
|
137
|
+
if (isDirectory && !relativePath.endsWith(path.sep)) {
|
|
138
|
+
relativePath += path.sep;
|
|
139
|
+
}
|
|
140
|
+
const normalized = relativePath.split(path.sep).join('/');
|
|
141
|
+
for (const spec of this.ignoreSpecs) {
|
|
142
|
+
if (spec.matches(normalized)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
getIgnoreSpecs() {
|
|
149
|
+
return [...this.ignoreSpecs];
|
|
150
|
+
}
|
|
151
|
+
reload() {
|
|
152
|
+
this.ignoreSpecs = [];
|
|
153
|
+
this.loadGitignoreFiles();
|
|
154
|
+
}
|
|
155
|
+
loadGitignoreFiles() {
|
|
156
|
+
const start = performance.now();
|
|
157
|
+
this.ignoreSpecs = [];
|
|
158
|
+
for (const gitignorePath of this.iterGitignoreFiles()) {
|
|
159
|
+
fileSystemLogger.debug('Processing .gitignore file', { path: gitignorePath });
|
|
160
|
+
const spec = this.createIgnoreSpec(gitignorePath);
|
|
161
|
+
if (spec) {
|
|
162
|
+
this.ignoreSpecs.push(spec);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const duration = performance.now() - start;
|
|
166
|
+
fileSystemLogger.debug('Loaded .gitignore specs', {
|
|
167
|
+
count: this.ignoreSpecs.length,
|
|
168
|
+
durationMs: Math.round(duration)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
*iterGitignoreFiles() {
|
|
172
|
+
const queue = [this.repoRoot];
|
|
173
|
+
while (queue.length > 0) {
|
|
174
|
+
const current = queue.shift();
|
|
175
|
+
if (!current) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (current !== this.repoRoot) {
|
|
179
|
+
const relative = path.relative(this.repoRoot, current);
|
|
180
|
+
if (this.shouldIgnore(relative)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let dirEntries;
|
|
185
|
+
try {
|
|
186
|
+
dirEntries = fs.readdirSync(current, { withFileTypes: true });
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (isPermissionError(error)) {
|
|
190
|
+
fileSystemLogger.debug('Skipping directory during gitignore discovery due to permission error', {
|
|
191
|
+
path: current,
|
|
192
|
+
error
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
for (const entry of dirEntries) {
|
|
199
|
+
const entryPath = path.join(current, entry.name);
|
|
200
|
+
if (entry.isDirectory() || (entry.isSymbolicLink() && isDirectorySafe(entryPath))) {
|
|
201
|
+
queue.push(entryPath);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (entry.isFile() && entry.name === '.gitignore') {
|
|
205
|
+
yield entryPath;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
createIgnoreSpec(gitignoreFilePath) {
|
|
211
|
+
let content;
|
|
212
|
+
try {
|
|
213
|
+
content = fs.readFileSync(gitignoreFilePath, 'utf-8');
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
fileSystemLogger.debug('Failed to read .gitignore file', {
|
|
217
|
+
path: gitignoreFilePath,
|
|
218
|
+
error
|
|
219
|
+
});
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
const patterns = this.parseGitignoreContent(content, path.dirname(gitignoreFilePath));
|
|
223
|
+
if (patterns.length === 0) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return new GitignoreSpec(gitignoreFilePath, patterns);
|
|
227
|
+
}
|
|
228
|
+
parseGitignoreContent(content, gitignoreDir) {
|
|
229
|
+
const patterns = [];
|
|
230
|
+
let relativeDir = path.relative(this.repoRoot, gitignoreDir);
|
|
231
|
+
if (relativeDir === '.') {
|
|
232
|
+
relativeDir = '';
|
|
233
|
+
}
|
|
234
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
235
|
+
let line = stripTrailingWhitespace(rawLine);
|
|
236
|
+
if (!line || line.trimStart().startsWith('#')) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
let isNegation = false;
|
|
240
|
+
if (line.startsWith('!')) {
|
|
241
|
+
isNegation = true;
|
|
242
|
+
line = line.slice(1);
|
|
243
|
+
}
|
|
244
|
+
line = line.trim();
|
|
245
|
+
if (!line) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (line.startsWith('\\#') || line.startsWith('\\!')) {
|
|
249
|
+
line = line.slice(1);
|
|
250
|
+
}
|
|
251
|
+
const anchored = line.startsWith('/');
|
|
252
|
+
if (anchored) {
|
|
253
|
+
line = line.slice(1);
|
|
254
|
+
}
|
|
255
|
+
let adjustedPattern;
|
|
256
|
+
if (relativeDir) {
|
|
257
|
+
if (anchored) {
|
|
258
|
+
adjustedPattern = path.join(relativeDir, line);
|
|
259
|
+
}
|
|
260
|
+
else if (line.startsWith('**/')) {
|
|
261
|
+
adjustedPattern = path.join(relativeDir, line);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
adjustedPattern = path.join(relativeDir, '**', line);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (anchored) {
|
|
268
|
+
adjustedPattern = `/${line}`;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
adjustedPattern = line;
|
|
272
|
+
}
|
|
273
|
+
if (isNegation) {
|
|
274
|
+
adjustedPattern = `!${adjustedPattern}`;
|
|
275
|
+
}
|
|
276
|
+
patterns.push(adjustedPattern.replace(/\\/g, '/'));
|
|
277
|
+
}
|
|
278
|
+
return patterns;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function normalizeCandidate(relativePath) {
|
|
282
|
+
let normalized = relativePath.replace(/\\/g, '/');
|
|
283
|
+
normalized = normalized.replace(/^\.\//, '');
|
|
284
|
+
if (normalized.startsWith('//')) {
|
|
285
|
+
normalized = normalized.slice(1);
|
|
286
|
+
}
|
|
287
|
+
return normalized;
|
|
288
|
+
}
|
|
289
|
+
function stripTrailingWhitespace(value) {
|
|
290
|
+
return value.replace(/[ \t]+$/, '');
|
|
291
|
+
}
|
|
292
|
+
function isDirectorySafe(candidatePath) {
|
|
293
|
+
try {
|
|
294
|
+
return fs.statSync(candidatePath).isDirectory();
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
if (isPermissionError(error)) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
if (isErrno(error, 'ENOENT') || isErrno(error, 'ENOTDIR')) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function isPermissionError(error) {
|
|
307
|
+
if (!error || typeof error !== 'object') {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const code = error.code;
|
|
311
|
+
return code === 'EACCES' || code === 'EPERM';
|
|
312
|
+
}
|
|
313
|
+
function isErrno(error, errno) {
|
|
314
|
+
return Boolean(error && typeof error === 'object' && error.code === errno);
|
|
315
|
+
}
|
|
316
|
+
function isPathInsideRoot(absolutePath, root) {
|
|
317
|
+
const normalizedRoot = path.resolve(root);
|
|
318
|
+
const normalizedPath = path.resolve(absolutePath);
|
|
319
|
+
return normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}${path.sep}`);
|
|
320
|
+
}
|
|
321
|
+
export function matchPath(relativePath, matcher, rootPath = '') {
|
|
322
|
+
let normalized = String(relativePath).replace(/\\/g, '/');
|
|
323
|
+
if (!normalized.startsWith('/')) {
|
|
324
|
+
normalized = `/${normalized}`;
|
|
325
|
+
}
|
|
326
|
+
const absolutePath = rootPath
|
|
327
|
+
? path.resolve(rootPath, relativePath)
|
|
328
|
+
: path.resolve(relativePath);
|
|
329
|
+
if (isDirectorySafe(absolutePath) && !normalized.endsWith('/')) {
|
|
330
|
+
normalized = `${normalized}/`;
|
|
331
|
+
}
|
|
332
|
+
const withoutLeadingSlash = normalized.slice(1);
|
|
333
|
+
const candidates = new Set();
|
|
334
|
+
if (withoutLeadingSlash) {
|
|
335
|
+
candidates.add(withoutLeadingSlash);
|
|
336
|
+
}
|
|
337
|
+
if (withoutLeadingSlash.endsWith('/')) {
|
|
338
|
+
const withoutTrailing = withoutLeadingSlash.slice(0, -1);
|
|
339
|
+
if (withoutTrailing) {
|
|
340
|
+
candidates.add(withoutTrailing);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else if (withoutLeadingSlash) {
|
|
344
|
+
candidates.add(`${withoutLeadingSlash}/`);
|
|
345
|
+
}
|
|
346
|
+
for (const candidate of candidates) {
|
|
347
|
+
if (matcher.ignores(candidate)) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface MinimalYamlDocument {
|
|
2
|
+
toString(): string;
|
|
3
|
+
set?(key: string, value: unknown): void;
|
|
4
|
+
toJSON?(): unknown;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export type YamlObject = Record<string, unknown>;
|
|
8
|
+
export type YamlDocument = MinimalYamlDocument;
|
|
9
|
+
export declare function loadYaml(pathname: string, preserveComments?: boolean): YamlObject | YamlDocument;
|
|
10
|
+
export declare function saveYaml(pathname: string, data: YamlObject | YamlDocument, preserveComments?: boolean): void;
|
|
11
|
+
export {};
|