@soederpop/luca 0.0.5 → 0.0.7
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/CLAUDE.md +10 -1
- package/bun.lock +1 -1
- package/commands/build-bootstrap.ts +78 -0
- package/commands/build-scaffolds.ts +24 -2
- package/commands/try-all-challenges.ts +543 -0
- package/commands/try-challenge.ts +100 -0
- package/docs/README.md +52 -80
- package/docs/TABLE-OF-CONTENTS.md +82 -51
- package/docs/apis/clients/elevenlabs.md +232 -8
- package/docs/apis/clients/graph.md +59 -8
- package/docs/apis/clients/openai.md +362 -2
- package/docs/apis/clients/rest.md +122 -2
- package/docs/apis/clients/websocket.md +71 -17
- package/docs/apis/features/agi/assistant.md +9 -3
- package/docs/apis/features/agi/assistants-manager.md +2 -2
- package/docs/apis/features/agi/claude-code.md +153 -14
- package/docs/apis/features/agi/conversation-history.md +15 -3
- package/docs/apis/features/agi/conversation.md +133 -20
- package/docs/apis/features/agi/openai-codex.md +90 -12
- package/docs/apis/features/agi/skills-library.md +23 -5
- package/docs/apis/features/node/container-link.md +59 -0
- package/docs/apis/features/node/content-db.md +1 -1
- package/docs/apis/features/node/disk-cache.md +1 -1
- package/docs/apis/features/node/dns.md +1 -0
- package/docs/apis/features/node/docker.md +2 -1
- package/docs/apis/features/node/esbuild.md +4 -3
- package/docs/apis/features/node/file-manager.md +13 -4
- package/docs/apis/features/node/fs.md +726 -171
- package/docs/apis/features/node/git.md +1 -0
- package/docs/apis/features/node/google-auth.md +23 -4
- package/docs/apis/features/node/google-calendar.md +14 -2
- package/docs/apis/features/node/google-docs.md +15 -2
- package/docs/apis/features/node/google-drive.md +21 -3
- package/docs/apis/features/node/google-sheets.md +14 -2
- package/docs/apis/features/node/grep.md +2 -0
- package/docs/apis/features/node/helpers.md +29 -0
- package/docs/apis/features/node/ink.md +2 -2
- package/docs/apis/features/node/networking.md +39 -4
- package/docs/apis/features/node/os.md +28 -0
- package/docs/apis/features/node/postgres.md +26 -4
- package/docs/apis/features/node/proc.md +37 -28
- package/docs/apis/features/node/process-manager.md +33 -5
- package/docs/apis/features/node/repl.md +1 -1
- package/docs/apis/features/node/runpod.md +1 -0
- package/docs/apis/features/node/secure-shell.md +7 -0
- package/docs/apis/features/node/semantic-search.md +12 -5
- package/docs/apis/features/node/sqlite.md +26 -4
- package/docs/apis/features/node/telegram.md +30 -5
- package/docs/apis/features/node/tts.md +17 -2
- package/docs/apis/features/node/ui.md +1 -1
- package/docs/apis/features/node/vault.md +4 -9
- package/docs/apis/features/node/vm.md +3 -12
- package/docs/apis/features/node/window-manager.md +128 -20
- package/docs/apis/features/web/asset-loader.md +13 -1
- package/docs/apis/features/web/container-link.md +59 -0
- package/docs/apis/features/web/esbuild.md +4 -3
- package/docs/apis/features/web/helpers.md +29 -0
- package/docs/apis/features/web/network.md +16 -2
- package/docs/apis/features/web/speech.md +16 -2
- package/docs/apis/features/web/vault.md +4 -9
- package/docs/apis/features/web/vm.md +3 -12
- package/docs/apis/features/web/voice.md +18 -1
- package/docs/apis/servers/express.md +18 -2
- package/docs/apis/servers/mcp.md +29 -4
- package/docs/apis/servers/websocket.md +34 -6
- package/docs/bootstrap/CLAUDE.md +100 -0
- package/docs/bootstrap/SKILL.md +222 -0
- package/docs/bootstrap/templates/about-command.ts +41 -0
- package/docs/bootstrap/templates/docs-models.ts +22 -0
- package/docs/bootstrap/templates/docs-readme.md +43 -0
- package/docs/bootstrap/templates/example-feature.ts +53 -0
- package/docs/bootstrap/templates/health-endpoint.ts +15 -0
- package/docs/bootstrap/templates/luca-cli.ts +25 -0
- package/docs/challenges/caching-proxy.md +16 -0
- package/docs/challenges/content-db-round-trip.md +14 -0
- package/docs/challenges/custom-command.md +9 -0
- package/docs/challenges/file-watcher-pipeline.md +11 -0
- package/docs/challenges/grep-audit-report.md +15 -0
- package/docs/challenges/multi-feature-dashboard.md +14 -0
- package/docs/challenges/process-orchestrator.md +17 -0
- package/docs/challenges/rest-api-server-with-client.md +12 -0
- package/docs/challenges/script-runner-with-vm.md +11 -0
- package/docs/challenges/simple-rest-api.md +15 -0
- package/docs/challenges/websocket-serve-and-client.md +11 -0
- package/docs/challenges/yaml-config-system.md +14 -0
- package/docs/command-system-overhaul.md +94 -0
- package/docs/examples/assistant/CORE.md +18 -0
- package/docs/examples/assistant/hooks.ts +3 -0
- package/docs/examples/assistant/tools.ts +10 -0
- package/docs/examples/window-manager-layouts.md +180 -0
- package/docs/in-memory-fs.md +4 -0
- package/docs/models.ts +13 -10
- package/docs/philosophy.md +4 -3
- package/docs/reports/console-hmr-design.md +170 -0
- package/docs/reports/helper-semantic-search.md +72 -0
- package/docs/scaffolds/client.md +29 -20
- package/docs/scaffolds/command.md +64 -50
- package/docs/scaffolds/endpoint.md +31 -36
- package/docs/scaffolds/feature.md +28 -18
- package/docs/scaffolds/selector.md +91 -0
- package/docs/scaffolds/server.md +18 -9
- package/docs/selectors.md +115 -0
- package/docs/sessions/custom-command/attempt-log-2.md +195 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
- package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
- package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
- package/docs/tutorials/00-bootstrap.md +148 -0
- package/docs/tutorials/07-endpoints.md +7 -7
- package/docs/tutorials/08-commands.md +153 -72
- package/luca.cli.ts +3 -0
- package/package.json +6 -5
- package/public/index.html +1430 -0
- package/scripts/examples/using-ollama.ts +2 -1
- package/scripts/update-introspection-data.ts +2 -2
- package/src/agi/endpoints/experts.ts +1 -1
- package/src/agi/features/assistant.ts +7 -0
- package/src/agi/features/assistants-manager.ts +5 -5
- package/src/agi/features/claude-code.ts +263 -3
- package/src/agi/features/conversation-history.ts +7 -1
- package/src/agi/features/conversation.ts +26 -3
- package/src/agi/features/openai-codex.ts +26 -2
- package/src/agi/features/openapi.ts +6 -1
- package/src/agi/features/skills-library.ts +9 -1
- package/src/bootstrap/generated.ts +540 -0
- package/src/cli/cli.ts +64 -21
- package/src/client.ts +23 -357
- package/src/clients/civitai/index.ts +1 -1
- package/src/clients/client-template.ts +1 -1
- package/src/clients/comfyui/index.ts +13 -2
- package/src/clients/elevenlabs/index.ts +2 -1
- package/src/clients/graph.ts +87 -0
- package/src/clients/openai/index.ts +10 -1
- package/src/clients/rest.ts +207 -0
- package/src/clients/websocket.ts +176 -0
- package/src/command.ts +281 -34
- package/src/commands/bootstrap.ts +181 -0
- package/src/commands/chat.ts +5 -4
- package/src/commands/describe.ts +225 -2
- package/src/commands/help.ts +35 -9
- package/src/commands/index.ts +3 -0
- package/src/commands/introspect.ts +92 -2
- package/src/commands/prompt.ts +5 -6
- package/src/commands/run.ts +33 -10
- package/src/commands/save-api-docs.ts +49 -0
- package/src/commands/scaffold.ts +169 -23
- package/src/commands/select.ts +94 -0
- package/src/commands/serve.ts +10 -1
- package/src/container.ts +15 -0
- package/src/endpoint.ts +19 -0
- package/src/graft.ts +181 -0
- package/src/introspection/generated.agi.ts +12458 -8968
- package/src/introspection/generated.node.ts +10573 -7145
- package/src/introspection/generated.web.ts +1 -1
- package/src/introspection/index.ts +26 -0
- package/src/node/container.ts +6 -7
- package/src/node/features/content-db.ts +49 -2
- package/src/node/features/disk-cache.ts +16 -9
- package/src/node/features/dns.ts +16 -3
- package/src/node/features/docker.ts +16 -4
- package/src/node/features/esbuild.ts +20 -0
- package/src/node/features/file-manager.ts +184 -29
- package/src/node/features/fs.ts +704 -248
- package/src/node/features/git.ts +21 -8
- package/src/node/features/grep.ts +23 -3
- package/src/node/features/helpers.ts +372 -43
- package/src/node/features/networking.ts +39 -4
- package/src/node/features/opener.ts +28 -15
- package/src/node/features/os.ts +76 -0
- package/src/node/features/port-exposer.ts +11 -1
- package/src/node/features/postgres.ts +17 -1
- package/src/node/features/proc.ts +4 -1
- package/src/node/features/python.ts +63 -14
- package/src/node/features/repl.ts +11 -7
- package/src/node/features/runpod.ts +16 -3
- package/src/node/features/secure-shell.ts +27 -2
- package/src/node/features/semantic-search.ts +12 -1
- package/src/node/features/ui.ts +5 -69
- package/src/node/features/vm.ts +17 -0
- package/src/node/features/window-manager.ts +68 -20
- package/src/node.ts +5 -0
- package/src/scaffolds/generated.ts +492 -290
- package/src/scaffolds/template.ts +9 -0
- package/src/schemas/base.ts +46 -5
- package/src/selector.ts +282 -0
- package/src/server.ts +11 -0
- package/src/servers/express.ts +27 -12
- package/src/servers/socket.ts +45 -11
- package/src/web/clients/socket.ts +4 -1
- package/src/web/container.ts +2 -1
- package/src/web/features/network.ts +7 -1
- package/src/web/features/voice-recognition.ts +16 -1
- package/test/clients-servers.test.ts +2 -1
- package/test/command.test.ts +267 -0
- package/test-integration/assistants-manager.test.ts +10 -20
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
- package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
- package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
- package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
- package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
- package/docs/examples/launcher-app-command-listener.md +0 -120
- package/docs/tasks/web-container-helper-discovery.md +0 -71
- package/docs/todos.md +0 -1
- package/scripts/test-command-listener.ts +0 -123
- package/src/node/features/launcher-app-command-listener.ts +0 -389
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setBuildTimeData, setContainerBuildTimeData } from './index.js';
|
|
2
2
|
|
|
3
3
|
// Auto-generated introspection registry data
|
|
4
|
-
// Generated at: 2026-03-
|
|
4
|
+
// Generated at: 2026-03-19T00:28:05.997Z
|
|
5
5
|
|
|
6
6
|
setBuildTimeData('features.containerLink', {
|
|
7
7
|
"id": "features.containerLink",
|
|
@@ -161,12 +161,38 @@ export function getContainerBuildTimeData(className: string): Partial<ContainerI
|
|
|
161
161
|
export function setBuildTimeData(key: string, data: HelperIntrospection) {
|
|
162
162
|
const existing = __INTROSPECTION__.get(key)
|
|
163
163
|
|
|
164
|
+
// Merge events: build-time AST provides descriptions, runtime Zod schemas provide arguments.
|
|
165
|
+
// For each event, preserve runtime arguments if the build-time entry has none.
|
|
166
|
+
const mergedEvents: Record<string, any> = { ...(data.events || {}) }
|
|
167
|
+
if (existing?.events) {
|
|
168
|
+
for (const [eventName, existingEvent] of Object.entries(existing.events)) {
|
|
169
|
+
if (mergedEvents[eventName]) {
|
|
170
|
+
// Build-time entry exists — merge in runtime arguments and description if build-time has none
|
|
171
|
+
const buildArgs = mergedEvents[eventName].arguments || {}
|
|
172
|
+
const runtimeArgs = (existingEvent as any).arguments || {}
|
|
173
|
+
const buildDesc = mergedEvents[eventName].description || ''
|
|
174
|
+
const runtimeDesc = (existingEvent as any).description || ''
|
|
175
|
+
const isGenericDesc = buildDesc.startsWith('Event emitted by ')
|
|
176
|
+
if (Object.keys(buildArgs).length === 0 && Object.keys(runtimeArgs).length > 0) {
|
|
177
|
+
mergedEvents[eventName] = { ...mergedEvents[eventName], arguments: runtimeArgs }
|
|
178
|
+
}
|
|
179
|
+
if (isGenericDesc && runtimeDesc) {
|
|
180
|
+
mergedEvents[eventName] = { ...mergedEvents[eventName], description: runtimeDesc }
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// Event only exists in runtime (from Zod schema) — preserve it
|
|
184
|
+
mergedEvents[eventName] = existingEvent
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
164
189
|
__INTROSPECTION__.set(key, {
|
|
165
190
|
...data,
|
|
166
191
|
// preserve runtime-derived className/state/options if registration already happened
|
|
167
192
|
className: data.className || existing?.className,
|
|
168
193
|
state: existing?.state || data.state || {},
|
|
169
194
|
options: existing?.options || data.options || {},
|
|
195
|
+
events: mergedEvents,
|
|
170
196
|
getters: data.getters || existing?.getters || {},
|
|
171
197
|
envVars: existing?.envVars || data.envVars || [],
|
|
172
198
|
examples: data.examples || existing?.examples,
|
package/src/node/container.ts
CHANGED
|
@@ -6,12 +6,16 @@ import type { FeatureOptions } from "./feature";
|
|
|
6
6
|
import { features, Feature } from "./feature";
|
|
7
7
|
import type { AvailableFeatures } from "../feature";
|
|
8
8
|
import { Client, type ClientsInterface } from "../client";
|
|
9
|
+
import "../clients/rest";
|
|
10
|
+
import "../clients/graph";
|
|
11
|
+
import "../clients/websocket";
|
|
9
12
|
import { Server, type ServersInterface } from "../server";
|
|
10
13
|
import "../servers/express";
|
|
11
14
|
import "../servers/socket";
|
|
12
15
|
import "../servers/mcp";
|
|
13
16
|
import { Command, type CommandsInterface } from "../command";
|
|
14
17
|
import { Endpoint, type EndpointsInterface } from "../endpoint";
|
|
18
|
+
import { Selector, type SelectorsInterface } from "../selector";
|
|
15
19
|
|
|
16
20
|
import minimist from "minimist";
|
|
17
21
|
import { omit, kebabCase, camelCase, mapKeys, castArray } from "lodash-es";
|
|
@@ -53,7 +57,6 @@ import "./features/google-sheets";
|
|
|
53
57
|
import "./features/google-calendar";
|
|
54
58
|
import "./features/google-docs";
|
|
55
59
|
import "./features/window-manager";
|
|
56
|
-
import "./features/launcher-app-command-listener";
|
|
57
60
|
import "./features/nlp";
|
|
58
61
|
import "./features/process-manager"
|
|
59
62
|
import "./features/tts";
|
|
@@ -98,7 +101,6 @@ import type { GoogleSheets } from './features/google-sheets';
|
|
|
98
101
|
import type { GoogleCalendar } from './features/google-calendar';
|
|
99
102
|
import type { GoogleDocs } from './features/google-docs';
|
|
100
103
|
import type { WindowManager } from './features/window-manager';
|
|
101
|
-
import type { LauncherAppCommandListener } from './features/launcher-app-command-listener';
|
|
102
104
|
import type { NLP } from './features/nlp';
|
|
103
105
|
import type { ProcessManager } from './features/process-manager'
|
|
104
106
|
import type { TTS } from './features/tts';
|
|
@@ -138,7 +140,6 @@ export {
|
|
|
138
140
|
type GoogleCalendar,
|
|
139
141
|
type GoogleDocs,
|
|
140
142
|
type WindowManager,
|
|
141
|
-
type LauncherAppCommandListener,
|
|
142
143
|
type NLP,
|
|
143
144
|
type ProcessManager,
|
|
144
145
|
type TTS,
|
|
@@ -204,7 +205,6 @@ export interface NodeFeatures extends AvailableFeatures {
|
|
|
204
205
|
googleCalendar: typeof GoogleCalendar;
|
|
205
206
|
googleDocs: typeof GoogleDocs;
|
|
206
207
|
windowManager: typeof WindowManager;
|
|
207
|
-
launcherAppCommandListener: typeof LauncherAppCommandListener;
|
|
208
208
|
nlp: typeof NLP;
|
|
209
209
|
processManager: typeof ProcessManager;
|
|
210
210
|
tts: typeof TTS;
|
|
@@ -214,7 +214,7 @@ export interface NodeFeatures extends AvailableFeatures {
|
|
|
214
214
|
dns: typeof Dns;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
export type ClientsAndServersInterface = ClientsInterface & ServersInterface & CommandsInterface & EndpointsInterface;
|
|
217
|
+
export type ClientsAndServersInterface = ClientsInterface & ServersInterface & CommandsInterface & EndpointsInterface & SelectorsInterface;
|
|
218
218
|
|
|
219
219
|
export interface NodeContainer extends ClientsAndServersInterface {}
|
|
220
220
|
|
|
@@ -253,7 +253,6 @@ export class NodeContainer<
|
|
|
253
253
|
googleCalendar?: GoogleCalendar;
|
|
254
254
|
googleDocs?: GoogleDocs;
|
|
255
255
|
windowManager?: WindowManager;
|
|
256
|
-
launcherAppCommandListener?: LauncherAppCommandListener;
|
|
257
256
|
nlp?: NLP;
|
|
258
257
|
processManager?: ProcessManager;
|
|
259
258
|
tts?: TTS;
|
|
@@ -286,7 +285,7 @@ export class NodeContainer<
|
|
|
286
285
|
}
|
|
287
286
|
});
|
|
288
287
|
|
|
289
|
-
this.use(Client).use(Server).use(Command).use(Endpoint);
|
|
288
|
+
this.use(Client).use(Server).use(Command).use(Endpoint).use(Selector);
|
|
290
289
|
}
|
|
291
290
|
|
|
292
291
|
override get Feature() {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Feature } from '../feature.js'
|
|
2
|
+
import * as contentbaseExports from 'contentbase'
|
|
2
3
|
import { parse, Collection, extractSections, type ModelDefinition } from 'contentbase'
|
|
3
4
|
import { z } from 'zod'
|
|
4
|
-
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
5
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
5
6
|
import { join, dirname } from 'node:path'
|
|
6
7
|
import { existsSync, readdirSync } from 'node:fs'
|
|
7
8
|
|
|
@@ -18,6 +19,10 @@ export const ContentDbOptionsSchema = FeatureOptionsSchema.extend({
|
|
|
18
19
|
export type ContentDbState = z.infer<typeof ContentDbStateSchema>
|
|
19
20
|
export type ContentDbOptions = z.infer<typeof ContentDbOptionsSchema>
|
|
20
21
|
|
|
22
|
+
export const ContentDbEventsSchema = FeatureEventsSchema.extend({
|
|
23
|
+
reloaded: z.tuple([]).describe('When the content collection is reloaded from disk'),
|
|
24
|
+
}).describe('ContentDb events')
|
|
25
|
+
|
|
21
26
|
/**
|
|
22
27
|
* Provides access to a Contentbase Collection for a folder of structured markdown files.
|
|
23
28
|
*
|
|
@@ -37,6 +42,7 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
37
42
|
static override shortcut = 'features.contentDb' as const
|
|
38
43
|
static override stateSchema = ContentDbStateSchema
|
|
39
44
|
static override optionsSchema = ContentDbOptionsSchema
|
|
45
|
+
static override eventsSchema = ContentDbEventsSchema
|
|
40
46
|
static { Feature.register(this, 'contentDb') }
|
|
41
47
|
|
|
42
48
|
override get initialState(): ContentDbState {
|
|
@@ -52,11 +58,52 @@ export class ContentDb extends Feature<ContentDbState, ContentDbOptions> {
|
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
_collection?: Collection
|
|
61
|
+
private _contentbaseSeeded = false
|
|
55
62
|
|
|
56
63
|
/** Returns the lazily-initialized Collection instance for the configured rootPath. */
|
|
57
64
|
get collection() {
|
|
58
65
|
if (this._collection) return this._collection
|
|
59
|
-
|
|
66
|
+
|
|
67
|
+
const opts: any = { rootPath: this.options.rootPath }
|
|
68
|
+
|
|
69
|
+
// When contentbase isn't in node_modules (e.g. compiled luca binary),
|
|
70
|
+
// provide a VM-based module loader so models.ts can resolve its imports
|
|
71
|
+
if (!this._canNativeImportContentbase()) {
|
|
72
|
+
opts.moduleLoader = (filePath: string) => {
|
|
73
|
+
this._seedContentbaseVirtualModules()
|
|
74
|
+
const vm = this.container.feature('vm') as any
|
|
75
|
+
return vm.loadModule(filePath)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this._collection = new Collection(opts)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Check if contentbase is resolvable via native import from the project root */
|
|
83
|
+
private _canNativeImportContentbase(): boolean {
|
|
84
|
+
const cwd = this.container.cwd
|
|
85
|
+
return existsSync(join(cwd, 'node_modules', 'contentbase'))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Seed the VM with virtual modules so models.ts can import from 'contentbase', 'zod', etc. */
|
|
89
|
+
private _seedContentbaseVirtualModules(): void {
|
|
90
|
+
if (this._contentbaseSeeded) return
|
|
91
|
+
this._contentbaseSeeded = true
|
|
92
|
+
|
|
93
|
+
const vm = this.container.feature('vm') as any
|
|
94
|
+
|
|
95
|
+
// Seed luca modules first (helpers does this for @soederpop/luca)
|
|
96
|
+
const helpers = this.container.feature('helpers') as any
|
|
97
|
+
if (helpers?.seedVirtualModules) {
|
|
98
|
+
helpers.seedVirtualModules()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Register contentbase barrel — everything the library exports
|
|
102
|
+
vm.defineModule('contentbase', contentbaseExports)
|
|
103
|
+
|
|
104
|
+
// Common deps that models.ts files tend to use
|
|
105
|
+
try { vm.defineModule('js-yaml', require('js-yaml')) } catch {}
|
|
106
|
+
try { vm.defineModule('mdast-util-to-string', require('mdast-util-to-string')) } catch {}
|
|
60
107
|
}
|
|
61
108
|
|
|
62
109
|
/** Returns the absolute resolved path to the collection root directory. */
|
|
@@ -36,15 +36,17 @@ export class DiskCache extends Feature<FeatureState,DiskCacheOptions> {
|
|
|
36
36
|
static override optionsSchema = DiskCacheOptionsSchema
|
|
37
37
|
static { Feature.register(this, 'diskCache') }
|
|
38
38
|
|
|
39
|
-
constructor(options: DiskCacheOptions, context: ContainerContext) {
|
|
40
|
-
super(options, context)
|
|
41
|
-
this._cache = this.create()
|
|
42
|
-
this.hide('_cache')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
39
|
/** Returns the underlying cacache instance configured with the cache directory path. */
|
|
46
40
|
get cache() {
|
|
47
|
-
|
|
41
|
+
if(this._cache) {
|
|
42
|
+
return this._cache
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const cache = this.create()
|
|
46
|
+
|
|
47
|
+
Object.defineProperty(this, '_cache', { value: cache, enumerable: false })
|
|
48
|
+
|
|
49
|
+
return cache
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/**
|
|
@@ -328,7 +330,7 @@ export class DiskCache extends Feature<FeatureState,DiskCacheOptions> {
|
|
|
328
330
|
|
|
329
331
|
/**
|
|
330
332
|
* Create a cacache instance with the specified path
|
|
331
|
-
* @param path - Optional cache directory path (defaults to options.path or
|
|
333
|
+
* @param path - Optional cache directory path (defaults to options.path or ~/.cache/luca/disk-cache-{cwdHash})
|
|
332
334
|
* @returns Configured cacache instance with all methods bound to the path
|
|
333
335
|
* @example
|
|
334
336
|
* ```typescript
|
|
@@ -336,7 +338,12 @@ export class DiskCache extends Feature<FeatureState,DiskCacheOptions> {
|
|
|
336
338
|
* ```
|
|
337
339
|
*/
|
|
338
340
|
create(path?: string) {
|
|
339
|
-
|
|
341
|
+
if (!path && !this.options.path) {
|
|
342
|
+
const cwdHash = this.container.utils.hashObject(this.container.cwd)
|
|
343
|
+
path = this.container.paths.resolve(process.env.HOME!, '.cache', 'luca', `disk-cache-${cwdHash}`)
|
|
344
|
+
} else {
|
|
345
|
+
path = path || this.options.path!
|
|
346
|
+
}
|
|
340
347
|
this.container.fs.ensureFolder(path)
|
|
341
348
|
const arg = (fn: (...args: any) => any) => partial(fn, path);
|
|
342
349
|
|
package/src/node/features/dns.ts
CHANGED
|
@@ -131,10 +131,23 @@ export class Dns extends Feature<DnsState, DnsOptions> {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
private _resolvedDigPath: string | null = null
|
|
135
|
+
|
|
134
136
|
get proc() {
|
|
135
137
|
return this.container.feature('proc')
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
/** Resolved path to the dig binary */
|
|
141
|
+
get digPath(): string {
|
|
142
|
+
if (this._resolvedDigPath) return this._resolvedDigPath
|
|
143
|
+
try {
|
|
144
|
+
this._resolvedDigPath = this.proc.exec('which dig').trim()
|
|
145
|
+
} catch {
|
|
146
|
+
this._resolvedDigPath = 'dig'
|
|
147
|
+
}
|
|
148
|
+
return this._resolvedDigPath
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
/**
|
|
139
152
|
* Checks whether the `dig` binary is available on the system.
|
|
140
153
|
*
|
|
@@ -148,7 +161,7 @@ export class Dns extends Feature<DnsState, DnsOptions> {
|
|
|
148
161
|
* ```
|
|
149
162
|
*/
|
|
150
163
|
async isAvailable(): Promise<boolean> {
|
|
151
|
-
const result = await this.proc.spawnAndCapture(
|
|
164
|
+
const result = await this.proc.spawnAndCapture(this.digPath, ['-v'])
|
|
152
165
|
// dig -v prints version to stderr and exits 0
|
|
153
166
|
return result.exitCode === 0
|
|
154
167
|
}
|
|
@@ -180,7 +193,7 @@ export class Dns extends Feature<DnsState, DnsOptions> {
|
|
|
180
193
|
*/
|
|
181
194
|
async resolve(domain: string, type: DnsRecordType, options: QueryOptions = {}): Promise<DnsQueryResult> {
|
|
182
195
|
const args = this.buildDigArgs(domain, type, options)
|
|
183
|
-
const result = await this.proc.spawnAndCapture(
|
|
196
|
+
const result = await this.proc.spawnAndCapture(this.digPath, args)
|
|
184
197
|
|
|
185
198
|
if (result.exitCode !== 0) {
|
|
186
199
|
throw new Error(`dig query failed: ${result.stderr || 'unknown error'}`)
|
|
@@ -446,7 +459,7 @@ export class Dns extends Feature<DnsState, DnsOptions> {
|
|
|
446
459
|
}
|
|
447
460
|
args.unshift('-x', ip)
|
|
448
461
|
|
|
449
|
-
const result = await this.proc.spawnAndCapture(
|
|
462
|
+
const result = await this.proc.spawnAndCapture(this.digPath, args)
|
|
450
463
|
|
|
451
464
|
if (result.exitCode !== 0) {
|
|
452
465
|
throw new Error(`dig reverse lookup failed: ${result.stderr || 'unknown error'}`)
|
|
@@ -101,10 +101,24 @@ export class Docker extends Feature<DockerState, DockerOptions> {
|
|
|
101
101
|
/**
|
|
102
102
|
* Get the proc feature for executing shell commands
|
|
103
103
|
*/
|
|
104
|
+
private _resolvedDockerPath: string | null = null
|
|
105
|
+
|
|
104
106
|
get proc() {
|
|
105
107
|
return this.container.feature('proc')
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
/** Resolve the docker binary path via `which`, caching the result. Options take precedence. */
|
|
111
|
+
get dockerPath(): string {
|
|
112
|
+
if (this.options.dockerPath) return this.options.dockerPath
|
|
113
|
+
if (this._resolvedDockerPath) return this._resolvedDockerPath
|
|
114
|
+
try {
|
|
115
|
+
this._resolvedDockerPath = this.proc.exec('which docker').trim()
|
|
116
|
+
} catch {
|
|
117
|
+
this._resolvedDockerPath = 'docker'
|
|
118
|
+
}
|
|
119
|
+
return this._resolvedDockerPath
|
|
120
|
+
}
|
|
121
|
+
|
|
108
122
|
/**
|
|
109
123
|
* Check if Docker is available and working.
|
|
110
124
|
*
|
|
@@ -117,8 +131,7 @@ export class Docker extends Feature<DockerState, DockerOptions> {
|
|
|
117
131
|
*/
|
|
118
132
|
async checkDockerAvailability(): Promise<boolean> {
|
|
119
133
|
try {
|
|
120
|
-
const
|
|
121
|
-
const result = await this.proc.spawnAndCapture(dockerPath, ['--version'])
|
|
134
|
+
const result = await this.proc.spawnAndCapture(this.dockerPath, ['--version'])
|
|
122
135
|
|
|
123
136
|
if (result.exitCode === 0) {
|
|
124
137
|
this.setState({ isDockerAvailable: true, lastError: undefined })
|
|
@@ -152,8 +165,7 @@ export class Docker extends Feature<DockerState, DockerOptions> {
|
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
try {
|
|
155
|
-
const
|
|
156
|
-
const result = await this.proc.spawnAndCapture(dockerPath, args)
|
|
168
|
+
const result = await this.proc.spawnAndCapture(this.dockerPath, args)
|
|
157
169
|
|
|
158
170
|
if (result.exitCode !== 0) {
|
|
159
171
|
this.setState({ lastError: result.stderr })
|
|
@@ -54,6 +54,26 @@ export class ESBuild extends Feature {
|
|
|
54
54
|
...options
|
|
55
55
|
})
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Bundle one or more entry points, resolving imports and requires into a single output.
|
|
60
|
+
* Supports Node platform by default so require() and Node builtins are handled.
|
|
61
|
+
* Returns in-memory output files unless write is enabled in options.
|
|
62
|
+
* @param entryPoints - File paths to bundle from
|
|
63
|
+
* @param options - esbuild BuildOptions overrides
|
|
64
|
+
* @returns The build result with outputFiles when write is false
|
|
65
|
+
*/
|
|
66
|
+
async bundle(entryPoints: string[], options?: esbuild.BuildOptions) {
|
|
67
|
+
return esbuild.build({
|
|
68
|
+
entryPoints,
|
|
69
|
+
bundle: true,
|
|
70
|
+
platform: 'node',
|
|
71
|
+
format: 'esm',
|
|
72
|
+
target: 'es2020',
|
|
73
|
+
write: false,
|
|
74
|
+
...options
|
|
75
|
+
})
|
|
76
|
+
}
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
export default ESBuild
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
2
|
+
import { FeatureEventsSchema, FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
|
|
3
3
|
import { State } from "../../state.js";
|
|
4
4
|
import { Feature } from "../feature.js";
|
|
5
|
-
import { parse, relative } from "path";
|
|
6
|
-
import { statSync } from "fs";
|
|
5
|
+
import { parse, relative, join as pathJoin } from "path";
|
|
6
|
+
import { statSync, readFileSync, existsSync } from "fs";
|
|
7
7
|
import micromatch from "micromatch";
|
|
8
8
|
import { castArray } from "lodash-es";
|
|
9
9
|
import chokidar from "chokidar";
|
|
@@ -37,6 +37,15 @@ export const FileManagerOptionsSchema = FeatureOptionsSchema.extend({
|
|
|
37
37
|
exclude: z.union([z.string(), z.array(z.string())]).optional().describe('Glob patterns to exclude from file scanning'),
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
export const FileManagerEventsSchema = FeatureEventsSchema.extend({
|
|
41
|
+
'file:change': z.tuple([
|
|
42
|
+
z.object({
|
|
43
|
+
type: z.enum(['add', 'change', 'delete']).describe('The type of file change'),
|
|
44
|
+
path: z.string().describe('Absolute path to the changed file'),
|
|
45
|
+
}).describe('File change event payload'),
|
|
46
|
+
]).describe('Emitted when a watched file is added, changed, or deleted'),
|
|
47
|
+
}).describe('FileManager events')
|
|
48
|
+
|
|
40
49
|
export type FileManagerState = z.infer<typeof FileManagerStateSchema>
|
|
41
50
|
export type FileManagerOptions = z.infer<typeof FileManagerOptionsSchema>
|
|
42
51
|
|
|
@@ -61,6 +70,7 @@ export class FileManager<
|
|
|
61
70
|
static override shortcut = 'features.fileManager' as const
|
|
62
71
|
static override stateSchema = FileManagerStateSchema
|
|
63
72
|
static override optionsSchema = FileManagerOptionsSchema
|
|
73
|
+
static override eventsSchema = FileManagerEventsSchema
|
|
64
74
|
static { Feature.register(this, 'fileManager') }
|
|
65
75
|
|
|
66
76
|
files: State<Record<string, File>> = new State<Record<string, File>>({
|
|
@@ -135,6 +145,11 @@ export class FileManager<
|
|
|
135
145
|
return !!this.state.get("watching");
|
|
136
146
|
}
|
|
137
147
|
|
|
148
|
+
/** Returns the list of directories currently being watched. */
|
|
149
|
+
get watchedPaths(): string[] {
|
|
150
|
+
return this.state.get("watchedPaths") || [];
|
|
151
|
+
}
|
|
152
|
+
|
|
138
153
|
/**
|
|
139
154
|
* Starts the file manager and scans the files in the project.
|
|
140
155
|
* @param {object} [options={}] - Options for the file manager
|
|
@@ -154,7 +169,11 @@ export class FileManager<
|
|
|
154
169
|
}
|
|
155
170
|
|
|
156
171
|
try {
|
|
157
|
-
await this.
|
|
172
|
+
const loaded = await this.loadFromCache();
|
|
173
|
+
if (!loaded) {
|
|
174
|
+
await this.scanFiles(options);
|
|
175
|
+
await this.writeToCache();
|
|
176
|
+
}
|
|
158
177
|
} catch (error) {
|
|
159
178
|
console.error(error);
|
|
160
179
|
this.state.set("failed", true);
|
|
@@ -166,6 +185,117 @@ export class FileManager<
|
|
|
166
185
|
return this;
|
|
167
186
|
}
|
|
168
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Attempts to load the file index from disk cache.
|
|
190
|
+
* Only uses cache when in a clean git repo (sha hasn't changed, no dirty files).
|
|
191
|
+
* @returns true if cache was loaded successfully, false otherwise
|
|
192
|
+
*/
|
|
193
|
+
/**
|
|
194
|
+
* Reads the current git SHA by reading .git/HEAD directly,
|
|
195
|
+
* avoiding the ~19ms cost of shelling out to `git rev-parse HEAD`.
|
|
196
|
+
*/
|
|
197
|
+
private readGitShaFast(): string | null {
|
|
198
|
+
try {
|
|
199
|
+
const { git } = this.container;
|
|
200
|
+
if (!git.isRepo) return null;
|
|
201
|
+
|
|
202
|
+
const gitDir = pathJoin(git.repoRoot, '.git');
|
|
203
|
+
const head = readFileSync(pathJoin(gitDir, 'HEAD'), 'utf8').trim();
|
|
204
|
+
|
|
205
|
+
// Detached HEAD — already a sha
|
|
206
|
+
if (!head.startsWith('ref: ')) return head;
|
|
207
|
+
|
|
208
|
+
// Resolve the ref
|
|
209
|
+
const refPath = pathJoin(gitDir, head.slice(5));
|
|
210
|
+
if (existsSync(refPath)) {
|
|
211
|
+
return readFileSync(refPath, 'utf8').trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Packed refs fallback
|
|
215
|
+
const packedRefsPath = pathJoin(gitDir, 'packed-refs');
|
|
216
|
+
if (existsSync(packedRefsPath)) {
|
|
217
|
+
const ref = head.slice(5);
|
|
218
|
+
const packed = readFileSync(packedRefsPath, 'utf8');
|
|
219
|
+
const match = packed.match(new RegExp(`^([0-9a-f]{40}) ${ref}`, 'm'));
|
|
220
|
+
if (match) return match[1];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return null;
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private async loadFromCache(): Promise<boolean> {
|
|
230
|
+
try {
|
|
231
|
+
const sha = this.readGitShaFast();
|
|
232
|
+
if (!sha) return false;
|
|
233
|
+
|
|
234
|
+
const cache = this.container.feature('diskCache') as any;
|
|
235
|
+
const cacheKey = `file-index:${sha}`;
|
|
236
|
+
|
|
237
|
+
if (!(await cache.has(cacheKey))) return false;
|
|
238
|
+
|
|
239
|
+
const cached = await cache.get(cacheKey, true) as { dirs: Record<string, number>, files: Record<string, any> };
|
|
240
|
+
if (!cached?.files || !cached?.dirs) return false;
|
|
241
|
+
|
|
242
|
+
// Check if any directory mtime has changed — catches new/deleted/renamed files
|
|
243
|
+
for (const [dir, cachedMtimeMs] of Object.entries(cached.dirs)) {
|
|
244
|
+
try {
|
|
245
|
+
const current = statSync(dir).mtimeMs;
|
|
246
|
+
if (current !== cachedMtimeMs) return false;
|
|
247
|
+
} catch {
|
|
248
|
+
// Directory no longer exists
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const [relativePath, file] of Object.entries(cached.files)) {
|
|
254
|
+
this.files.set(relativePath, {
|
|
255
|
+
...file as File,
|
|
256
|
+
modifiedAt: new Date((file as any).modifiedAt),
|
|
257
|
+
createdAt: new Date((file as any).createdAt),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return true;
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Writes the current file index to disk cache, keyed by git sha.
|
|
269
|
+
* Stores directory mtimes alongside file data so the cache can be
|
|
270
|
+
* invalidated when files are added/removed without a new commit.
|
|
271
|
+
*/
|
|
272
|
+
private async writeToCache(): Promise<void> {
|
|
273
|
+
try {
|
|
274
|
+
const sha = this.readGitShaFast();
|
|
275
|
+
if (!sha) return;
|
|
276
|
+
|
|
277
|
+
const cache = this.container.feature('diskCache') as any;
|
|
278
|
+
const cacheKey = `file-index:${sha}`;
|
|
279
|
+
|
|
280
|
+
// Collect unique directories and their mtimes
|
|
281
|
+
const dirs: Record<string, number> = {};
|
|
282
|
+
const files: Record<string, any> = {};
|
|
283
|
+
|
|
284
|
+
for (const [key, file] of this.files.entries()) {
|
|
285
|
+
files[key] = file;
|
|
286
|
+
if (!dirs[file.dirname]) {
|
|
287
|
+
try {
|
|
288
|
+
dirs[file.dirname] = statSync(file.dirname).mtimeMs;
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await cache.set(cacheKey, { dirs, files });
|
|
294
|
+
} catch {
|
|
295
|
+
// Cache write failure is non-fatal
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
169
299
|
/**
|
|
170
300
|
* Scans the files in the project and updates the file manager state.
|
|
171
301
|
* @param {object} [options={}] - Options for the file manager
|
|
@@ -252,14 +382,33 @@ export class FileManager<
|
|
|
252
382
|
return this.watcher?.getWatched() || {};
|
|
253
383
|
}
|
|
254
384
|
|
|
255
|
-
/**
|
|
256
|
-
* Watches
|
|
385
|
+
/**
|
|
386
|
+
* Watches directories for file changes. Can be called multiple times to add
|
|
387
|
+
* more directories to an existing watcher. Tracks all watched paths in state.
|
|
388
|
+
*
|
|
389
|
+
* When called without `paths`, watches the project's `directoryIds` (default behavior).
|
|
390
|
+
* When called with `paths`, watches only those specific directories/globs.
|
|
391
|
+
* Subsequent calls add to the existing watcher — they never replace what's already watched.
|
|
392
|
+
*
|
|
257
393
|
* @param {object} [options={}] - Options for the file manager
|
|
394
|
+
* @param {string | string[]} [options.paths] - Specific directories or globs to watch. Defaults to project directoryIds.
|
|
258
395
|
* @param {string | string[]} [options.exclude] - The patterns to exclude from the watch
|
|
259
|
-
* @returns {Promise<void>}
|
|
396
|
+
* @returns {Promise<void>}
|
|
260
397
|
*/
|
|
261
|
-
async watch(options: { exclude?: string | string[] } = {}) {
|
|
262
|
-
|
|
398
|
+
async watch(options: { paths?: string | string[]; exclude?: string | string[] } = {}) {
|
|
399
|
+
const pathsToWatch = castArray(options.paths || this.directoryIds.map(id => this.container.paths.resolve(id)))
|
|
400
|
+
.map(p => this.container.paths.resolve(p));
|
|
401
|
+
|
|
402
|
+
// If already watching, just add the new paths
|
|
403
|
+
if (this.isWatching && this.watcher) {
|
|
404
|
+
const currentPaths: string[] = this.state.get("watchedPaths") || [];
|
|
405
|
+
const newPaths = pathsToWatch.filter(p => !currentPaths.includes(p));
|
|
406
|
+
|
|
407
|
+
if (newPaths.length) {
|
|
408
|
+
this.watcher.add(newPaths);
|
|
409
|
+
this.state.set("watchedPaths", [...currentPaths, ...newPaths]);
|
|
410
|
+
}
|
|
411
|
+
|
|
263
412
|
return;
|
|
264
413
|
}
|
|
265
414
|
|
|
@@ -273,11 +422,7 @@ export class FileManager<
|
|
|
273
422
|
|
|
274
423
|
exclude.push(...castArray(this.options.exclude!).filter((v) => v?.length));
|
|
275
424
|
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
const watcher = chokidar.watch(
|
|
279
|
-
this.directoryIds.map(id => this.container.paths.resolve(id))
|
|
280
|
-
, {
|
|
425
|
+
const watcher = chokidar.watch(pathsToWatch, {
|
|
281
426
|
ignoreInitial: true,
|
|
282
427
|
persistent: true,
|
|
283
428
|
ignored: [
|
|
@@ -292,17 +437,17 @@ export class FileManager<
|
|
|
292
437
|
|
|
293
438
|
watcher
|
|
294
439
|
.on("add", (path) => {
|
|
440
|
+
this.updateFile(path);
|
|
295
441
|
this.emit("file:change", {
|
|
296
442
|
type: "add",
|
|
297
443
|
path,
|
|
298
444
|
});
|
|
299
|
-
this.updateFile(path);
|
|
300
445
|
})
|
|
301
446
|
.on("change", (path) => {
|
|
302
447
|
this.updateFile(path);
|
|
303
448
|
this.emit("file:change", {
|
|
304
|
-
path,
|
|
305
449
|
type: "change",
|
|
450
|
+
path,
|
|
306
451
|
});
|
|
307
452
|
})
|
|
308
453
|
.on("unlink", (path) => {
|
|
@@ -315,6 +460,7 @@ export class FileManager<
|
|
|
315
460
|
|
|
316
461
|
watcher.on("ready", () => {
|
|
317
462
|
this.state.set("watching", true);
|
|
463
|
+
this.state.set("watchedPaths", pathsToWatch);
|
|
318
464
|
});
|
|
319
465
|
|
|
320
466
|
this.watcher = watcher;
|
|
@@ -328,26 +474,35 @@ export class FileManager<
|
|
|
328
474
|
if (this.watcher) {
|
|
329
475
|
this.watcher.close();
|
|
330
476
|
this.state.set("watching", false);
|
|
477
|
+
this.state.set("watchedPaths", []);
|
|
331
478
|
this.watcher = null;
|
|
332
479
|
}
|
|
333
480
|
}
|
|
334
481
|
|
|
335
482
|
async updateFile(path: string) {
|
|
336
|
-
// Reuse the logic from the scanFiles method to update a single file
|
|
337
483
|
const absolutePath = this.container.paths.resolve(path);
|
|
338
484
|
const { name, ext, dir } = parse(absolutePath);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const stats = statSync(absolutePath);
|
|
488
|
+
this.files.set(path, {
|
|
489
|
+
dirname: dir,
|
|
490
|
+
absolutePath,
|
|
491
|
+
relativePath: path,
|
|
492
|
+
name,
|
|
493
|
+
extension: ext,
|
|
494
|
+
size: stats.size,
|
|
495
|
+
modifiedAt: stats.mtime,
|
|
496
|
+
createdAt: stats.birthtime,
|
|
497
|
+
});
|
|
498
|
+
} catch (err: any) {
|
|
499
|
+
// File may have been moved or deleted by an event handler — remove from index gracefully
|
|
500
|
+
if (err.code === 'ENOENT') {
|
|
501
|
+
this.files.delete(path);
|
|
502
|
+
} else {
|
|
503
|
+
throw err;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
351
506
|
}
|
|
352
507
|
|
|
353
508
|
async removeFile(path: string) {
|