@myagentroam/node 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connector.js +1 -1
- package/dist/database.js +53 -31
- package/dist/migrations/v021.js +64 -0
- package/dist/service/node-workspace-coordinator.js +21 -3
- package/dist/service/skill-directory-service.js +2 -2
- package/dist/service/skill-install-service.js +15 -7
- package/dist/service/skill-node-operation-service.js +5 -4
- package/dist/service/workspace-change-service.js +29 -13
- package/dist/service/workspace-file-service.js +3 -1
- package/dist/service/workspace-git-exclude-service.js +59 -32
- package/dist/service/workspace-service.js +34 -9
- package/dist/service/workspace-watch-snapshot-service.js +35 -5
- package/dist/service/workspace-workbench-service.js +23 -11
- package/dist/workspace.js +27 -3
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -1343,7 +1343,7 @@ export class NodeConnector {
|
|
|
1343
1343
|
this.workspaceCoordinator.clearWatches(workspaceId);
|
|
1344
1344
|
this.workspaceWorkbenchService.clearWorkspace(workspaceId);
|
|
1345
1345
|
this.promptCompletion?.invalidateWorkspace(workspaceId);
|
|
1346
|
-
});
|
|
1346
|
+
}, (workspaceId) => this.workspaceCoordinator.gitPolicyChanged(workspaceId));
|
|
1347
1347
|
this.workspaceWorkbenchService = new WorkspaceWorkbenchService({
|
|
1348
1348
|
requireWorkspace: (workspaceId) => this.workspaceCoordinator.require(workspaceId),
|
|
1349
1349
|
inspectWorkspace: (workspace) => this.workspaceCoordinator.inspectMetadata(workspace),
|
package/dist/database.js
CHANGED
|
@@ -2,6 +2,7 @@ import { NODE_SCHEMA_MIGRATION_V17 } from './migrations/v017.js';
|
|
|
2
2
|
import { NODE_SCHEMA_MIGRATION_V18 } from './migrations/v018.js';
|
|
3
3
|
import { NODE_SCHEMA_MIGRATION_V19 } from './migrations/v019.js';
|
|
4
4
|
import { NODE_SCHEMA_MIGRATION_V20 } from './migrations/v020.js';
|
|
5
|
+
import { NODE_SCHEMA_MIGRATION_V21 } from './migrations/v021.js';
|
|
5
6
|
import { normalizeSessionPrompt } from './util/personal-instructions.js';
|
|
6
7
|
import { randomUUID } from 'node:crypto';
|
|
7
8
|
import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
|
|
@@ -23,7 +24,8 @@ export const NODE_SCHEMA_MIGRATIONS = [
|
|
|
23
24
|
NODE_SCHEMA_MIGRATION_V17,
|
|
24
25
|
NODE_SCHEMA_MIGRATION_V18,
|
|
25
26
|
NODE_SCHEMA_MIGRATION_V19,
|
|
26
|
-
NODE_SCHEMA_MIGRATION_V20
|
|
27
|
+
NODE_SCHEMA_MIGRATION_V20,
|
|
28
|
+
NODE_SCHEMA_MIGRATION_V21
|
|
27
29
|
];
|
|
28
30
|
const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
|
|
29
31
|
const OBSOLETE_NODE_SCHEMA_V16 = 16;
|
|
@@ -71,27 +73,22 @@ export class NodeDatabase {
|
|
|
71
73
|
this.raw.close();
|
|
72
74
|
}
|
|
73
75
|
getRunnerDefaults() {
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
const parsed = runnerDefaultConfigurationSchema.safeParse({
|
|
79
|
-
runner: stringValue(row, 'runner'),
|
|
80
|
-
model: stringValue(row, 'model'),
|
|
81
|
-
effort: stringValue(row, 'effort'),
|
|
82
|
-
access: stringValue(row, 'access')
|
|
83
|
-
});
|
|
76
|
+
return Object.entries(this.getNodeSettings().runnerDefaults ?? {})
|
|
77
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
78
|
+
.flatMap(([runner, settings]) => {
|
|
79
|
+
const parsed = runnerDefaultConfigurationSchema.safeParse({ runner, ...settings });
|
|
84
80
|
return parsed.success ? [parsed.data] : [];
|
|
85
81
|
});
|
|
86
82
|
}
|
|
83
|
+
getNodeSettings() {
|
|
84
|
+
const row = this.raw.prepare('SELECT settings FROM node_settings WHERE id = 1').get();
|
|
85
|
+
return JSON.parse(stringValue(row, 'settings'));
|
|
86
|
+
}
|
|
87
87
|
saveRunnerDefaults(input) {
|
|
88
|
+
const { runner, ...settings } = runnerDefaultConfigurationSchema.parse(input);
|
|
88
89
|
this.raw
|
|
89
|
-
.prepare(`
|
|
90
|
-
|
|
91
|
-
ON CONFLICT(runner) DO UPDATE SET
|
|
92
|
-
model = excluded.model, effort = excluded.effort,
|
|
93
|
-
access = excluded.access`)
|
|
94
|
-
.run(input.runner, input.model, input.effort, input.access);
|
|
90
|
+
.prepare(`UPDATE node_settings SET settings = json_patch(settings, ?) WHERE id = 1`)
|
|
91
|
+
.run(JSON.stringify({ runnerDefaults: { [runner]: settings } }));
|
|
95
92
|
return input;
|
|
96
93
|
}
|
|
97
94
|
listMcpInstallations(targetKind, targetId = '') {
|
|
@@ -188,45 +185,64 @@ export class NodeDatabase {
|
|
|
188
185
|
}
|
|
189
186
|
saveWorkspace(input) {
|
|
190
187
|
const current = this.raw
|
|
191
|
-
.prepare('SELECT id,
|
|
188
|
+
.prepare('SELECT id, settings FROM workspaces WHERE path = ?')
|
|
192
189
|
.get(input.path);
|
|
190
|
+
const settings = current === undefined
|
|
191
|
+
? {
|
|
192
|
+
git: {
|
|
193
|
+
preserveGitConfig: input.preserveGitConfig ?? false,
|
|
194
|
+
includeParentRepository: input.includeParentRepository ?? true
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
: JSON.parse(stringValue(current, 'settings'));
|
|
193
198
|
const workspace = {
|
|
194
199
|
id: current === undefined ? (input.id ?? randomUUID()) : stringValue(current, 'id'),
|
|
195
200
|
path: input.path,
|
|
196
201
|
kind: input.kind,
|
|
197
202
|
gitSummary: input.gitSummary,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
settings,
|
|
204
|
+
includeParentRepository: settings.git?.includeParentRepository ?? true,
|
|
205
|
+
preserveGitConfig: settings.git?.preserveGitConfig ?? false
|
|
201
206
|
};
|
|
202
207
|
const timestamp = this.now();
|
|
203
208
|
this.raw
|
|
204
|
-
.prepare(`INSERT INTO workspaces (id, path, kind, git_summary, created_at, updated_at,
|
|
209
|
+
.prepare(`INSERT INTO workspaces (id, path, kind, git_summary, created_at, updated_at, settings)
|
|
205
210
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
206
211
|
ON CONFLICT(path) DO UPDATE SET kind = excluded.kind,
|
|
207
212
|
git_summary = excluded.git_summary, updated_at = excluded.updated_at`)
|
|
208
|
-
.run(workspace.id, workspace.path, workspace.kind, workspace.gitSummary === null ? null : JSON.stringify(workspace.gitSummary), timestamp, timestamp,
|
|
213
|
+
.run(workspace.id, workspace.path, workspace.kind, workspace.gitSummary === null ? null : JSON.stringify(workspace.gitSummary), timestamp, timestamp, JSON.stringify(settings));
|
|
209
214
|
return workspace;
|
|
210
215
|
}
|
|
211
216
|
listWorkspaces() {
|
|
212
217
|
return this.raw
|
|
213
|
-
.prepare('SELECT id, path, kind, git_summary,
|
|
218
|
+
.prepare('SELECT id, path, kind, git_summary, settings FROM workspaces ORDER BY created_at ASC')
|
|
214
219
|
.all()
|
|
215
220
|
.map((row) => workspace(row));
|
|
216
221
|
}
|
|
217
222
|
getWorkspace(id) {
|
|
218
223
|
const row = this.raw
|
|
219
|
-
.prepare('SELECT id, path, kind, git_summary,
|
|
224
|
+
.prepare('SELECT id, path, kind, git_summary, settings FROM workspaces WHERE id = ?')
|
|
220
225
|
.get(id);
|
|
221
226
|
return row === undefined ? undefined : workspace(row);
|
|
222
227
|
}
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
getWorkspaceByPath(path) {
|
|
229
|
+
const row = this.raw
|
|
230
|
+
.prepare('SELECT id, path, kind, git_summary, settings FROM workspaces WHERE path = ?')
|
|
231
|
+
.get(path);
|
|
232
|
+
return row === undefined ? undefined : workspace(row);
|
|
233
|
+
}
|
|
234
|
+
/** Updates only supplied policy fields without replacing Workspace metadata. */
|
|
235
|
+
setWorkspaceGitPolicy(id, preserveGitConfig, includeParentRepository) {
|
|
225
236
|
if (this.getWorkspace(id) === undefined)
|
|
226
237
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
227
238
|
this.raw
|
|
228
|
-
.prepare('UPDATE workspaces SET
|
|
229
|
-
.run(
|
|
239
|
+
.prepare('UPDATE workspaces SET settings=json_patch(settings, ?), updated_at=? WHERE id=?')
|
|
240
|
+
.run(JSON.stringify({
|
|
241
|
+
git: {
|
|
242
|
+
preserveGitConfig,
|
|
243
|
+
...(includeParentRepository === undefined ? {} : { includeParentRepository })
|
|
244
|
+
}
|
|
245
|
+
}), this.now(), id);
|
|
230
246
|
return this.getWorkspace(id);
|
|
231
247
|
}
|
|
232
248
|
/** Unregisters a Workspace registration without deleting its local directory. */
|
|
@@ -474,7 +490,10 @@ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
|
|
|
474
490
|
throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
|
|
475
491
|
if (migratedVersions.some((version, index) => version !== NODE_SCHEMA_VERSION_V13 + index))
|
|
476
492
|
throw new Error('NODE_DATABASE_MIGRATION_HISTORY_INVALID');
|
|
477
|
-
|
|
493
|
+
if (current < NODE_SCHEMA_MIGRATION_V21.version)
|
|
494
|
+
assertNodeSchemaV13(database);
|
|
495
|
+
else
|
|
496
|
+
catalog[current - NODE_SCHEMA_VERSION_V13 - 1].assert(database);
|
|
478
497
|
for (const entry of catalog) {
|
|
479
498
|
if (entry.version <= current)
|
|
480
499
|
continue;
|
|
@@ -623,9 +642,12 @@ function acquireNodeMigrationLock(databasePath) {
|
|
|
623
642
|
}
|
|
624
643
|
function workspace(row) {
|
|
625
644
|
const summary = nullableString(row, 'git_summary');
|
|
645
|
+
const settings = JSON.parse(stringValue(row, 'settings'));
|
|
626
646
|
return {
|
|
627
647
|
id: stringValue(row, 'id'),
|
|
628
|
-
|
|
648
|
+
settings,
|
|
649
|
+
preserveGitConfig: settings.git?.preserveGitConfig ?? false,
|
|
650
|
+
includeParentRepository: settings.git?.includeParentRepository ?? true,
|
|
629
651
|
path: stringValue(row, 'path'),
|
|
630
652
|
kind: stringValue(row, 'kind'),
|
|
631
653
|
gitSummary: summary === null ? null : JSON.parse(summary)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { NODE_SCHEMA_V13 } from './v013.js';
|
|
3
|
+
import { NODE_SCHEMA_MIGRATION_V14 } from './v014.js';
|
|
4
|
+
import { NODE_SCHEMA_MIGRATION_V15 } from './v015.js';
|
|
5
|
+
import { NODE_SCHEMA_MIGRATION_V16 } from './v016.js';
|
|
6
|
+
import { NODE_SCHEMA_MIGRATION_V17 } from './v017.js';
|
|
7
|
+
import { NODE_SCHEMA_MIGRATION_V18 } from './v018.js';
|
|
8
|
+
import { NODE_SCHEMA_MIGRATION_V19 } from './v019.js';
|
|
9
|
+
import { NODE_SCHEMA_MIGRATION_V20 } from './v020.js';
|
|
10
|
+
const sql = `ALTER TABLE workspaces ADD COLUMN settings TEXT NOT NULL DEFAULT '{}'
|
|
11
|
+
CHECK (json_valid(settings) AND json_type(settings) = 'object');
|
|
12
|
+
UPDATE workspaces SET settings = json_object('git', json_object(
|
|
13
|
+
'preserveGitConfig', json(CASE WHEN preserve_git_config = 1 THEN 'true' ELSE 'false' END),
|
|
14
|
+
'includeParentRepository', json('true')));
|
|
15
|
+
ALTER TABLE workspaces DROP COLUMN preserve_git_config;
|
|
16
|
+
CREATE TABLE node_settings (
|
|
17
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
18
|
+
settings TEXT NOT NULL CHECK (json_valid(settings) AND json_type(settings) = 'object')
|
|
19
|
+
) STRICT;
|
|
20
|
+
INSERT INTO node_settings (id, settings)
|
|
21
|
+
SELECT 1, json_object('runnerDefaults', json_group_object(runner,
|
|
22
|
+
json_object('model', model, 'effort', effort, 'access', access))) FROM runner_defaults;
|
|
23
|
+
DROP TABLE runner_defaults;`;
|
|
24
|
+
export const NODE_SCHEMA_MIGRATION_V21 = {
|
|
25
|
+
version: 21,
|
|
26
|
+
sql,
|
|
27
|
+
preflight: (database) => NODE_SCHEMA_MIGRATION_V20.assert(database),
|
|
28
|
+
assert(database) {
|
|
29
|
+
const reference = new DatabaseSync(':memory:');
|
|
30
|
+
try {
|
|
31
|
+
reference.exec(NODE_SCHEMA_V13);
|
|
32
|
+
for (const migration of [
|
|
33
|
+
NODE_SCHEMA_MIGRATION_V14,
|
|
34
|
+
NODE_SCHEMA_MIGRATION_V15,
|
|
35
|
+
NODE_SCHEMA_MIGRATION_V16,
|
|
36
|
+
NODE_SCHEMA_MIGRATION_V17,
|
|
37
|
+
NODE_SCHEMA_MIGRATION_V18,
|
|
38
|
+
NODE_SCHEMA_MIGRATION_V19,
|
|
39
|
+
NODE_SCHEMA_MIGRATION_V20
|
|
40
|
+
])
|
|
41
|
+
reference.exec(migration.sql);
|
|
42
|
+
reference.exec(sql);
|
|
43
|
+
const schema = (db) => db
|
|
44
|
+
.prepare("SELECT type,name,sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type,name")
|
|
45
|
+
.all()
|
|
46
|
+
.map((row) => [
|
|
47
|
+
row.type,
|
|
48
|
+
row.name,
|
|
49
|
+
row.name === 'node_schema_migrations'
|
|
50
|
+
? String(row.sql).replace(/\s+/g, '')
|
|
51
|
+
: String(row.sql).replace(/\s+/g, ' ').trim()
|
|
52
|
+
]);
|
|
53
|
+
const expected = schema(reference);
|
|
54
|
+
const actual = schema(database);
|
|
55
|
+
const mismatch = expected.find((row) => !actual.some((value) => JSON.stringify(value) === JSON.stringify(row))) ??
|
|
56
|
+
actual.find((row) => !expected.some((value) => JSON.stringify(value) === JSON.stringify(row)));
|
|
57
|
+
if (mismatch !== undefined)
|
|
58
|
+
throw new Error(`NODE_DATABASE_SCHEMA_INVALID:${String(mismatch[1])}`);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
reference.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { nodeLog } from '../operational.js';
|
|
1
3
|
import { inspectWorkspace, normalizeWorkspacePath } from '../workspace.js';
|
|
2
4
|
import { ensureWorkspaceGitExclude } from './workspace-git-exclude-service.js';
|
|
3
5
|
/** Coordinates Workspace invalidation, inspection and persisted Terminal summaries. */
|
|
@@ -18,7 +20,7 @@ export class NodeWorkspaceCoordinator {
|
|
|
18
20
|
const config = this.options.config();
|
|
19
21
|
if (config === undefined)
|
|
20
22
|
throw new Error('NODE_CONFIG_UNAVAILABLE');
|
|
21
|
-
const inspection = await inspectWorkspace(workspace.path, config.allowedRoots);
|
|
23
|
+
const inspection = await inspectWorkspace(workspace.path, config.allowedRoots, workspace.includeParentRepository ?? true);
|
|
22
24
|
if (normalizeWorkspacePath(inspection.path) !== normalizeWorkspacePath(workspace.path))
|
|
23
25
|
throw new Error('WORKSPACE_NOT_ALLOWED');
|
|
24
26
|
return inspection;
|
|
@@ -29,8 +31,14 @@ export class NodeWorkspaceCoordinator {
|
|
|
29
31
|
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
|
30
32
|
if (normalizeWorkspacePath(inspection.path) !== normalizeWorkspacePath(workspace.path))
|
|
31
33
|
throw new Error('WORKSPACE_NOT_ALLOWED');
|
|
34
|
+
const current = this.require(workspace.id);
|
|
35
|
+
if ((current.includeParentRepository ?? true) !== (workspace.includeParentRepository ?? true))
|
|
36
|
+
return current;
|
|
32
37
|
if (workspace.kind !== 'GIT_WORKSPACE' && inspection.kind === 'GIT_WORKSPACE')
|
|
33
|
-
await ensureWorkspaceGitExclude(workspace.path);
|
|
38
|
+
await ensureWorkspaceGitExclude(workspace.path, workspace.includeParentRepository ?? true);
|
|
39
|
+
if ((this.require(workspace.id).includeParentRepository ?? true) !==
|
|
40
|
+
(workspace.includeParentRepository ?? true))
|
|
41
|
+
return this.require(workspace.id);
|
|
34
42
|
const saved = database.saveWorkspace({
|
|
35
43
|
id: workspace.id,
|
|
36
44
|
path: workspace.path,
|
|
@@ -57,6 +65,14 @@ export class NodeWorkspaceCoordinator {
|
|
|
57
65
|
invalidateChanges(workspaceId) {
|
|
58
66
|
this.options.changes.invalidate(workspaceId);
|
|
59
67
|
}
|
|
68
|
+
gitPolicyChanged(workspaceId) {
|
|
69
|
+
this.invalidate(workspaceId);
|
|
70
|
+
const workspace = this.require(workspaceId);
|
|
71
|
+
void this.inspectMetadata(workspace)
|
|
72
|
+
.then((inspection) => this.commitMetadata(workspace, inspection))
|
|
73
|
+
.then(() => this.invalidate(workspaceId))
|
|
74
|
+
.catch(() => nodeLog('workspace.git-policy.refresh-failed', { workspaceId }));
|
|
75
|
+
}
|
|
60
76
|
persistTerminal(summary) {
|
|
61
77
|
const database = this.options.database();
|
|
62
78
|
if (database === undefined)
|
|
@@ -87,7 +103,9 @@ export class NodeWorkspaceCoordinator {
|
|
|
87
103
|
return;
|
|
88
104
|
}
|
|
89
105
|
try {
|
|
90
|
-
const
|
|
106
|
+
const path = await realpath(requestedPath);
|
|
107
|
+
const current = this.options.database()?.getWorkspaceByPath(path);
|
|
108
|
+
const inspection = await inspectWorkspace(path, config.allowedRoots, current?.includeParentRepository ?? true);
|
|
91
109
|
this.options.send('workspace.inspect.result', inspection, envelope.id);
|
|
92
110
|
}
|
|
93
111
|
catch {
|
|
@@ -22,11 +22,11 @@ export class SkillDirectoryService {
|
|
|
22
22
|
: agents.map(withoutSource)
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
async inspectWorkspace(workspace) {
|
|
25
|
+
async inspectWorkspace(workspace, includeParentRepository = true) {
|
|
26
26
|
const agents = await scanRoot(path.join(workspace, '.agents', 'skills'), 'AGENTS');
|
|
27
27
|
const claude = await scanRoot(path.join(workspace, '.claude', 'skills'), 'CLAUDE');
|
|
28
28
|
const skills = mergeWorkspaceCopies(agents, claude);
|
|
29
|
-
const gitExcludeBroken = await hasBrokenWorkspaceSkillGitExclude(workspace, skills.map((skill) => skill.name));
|
|
29
|
+
const gitExcludeBroken = await hasBrokenWorkspaceSkillGitExclude(workspace, skills.map((skill) => skill.name), includeParentRepository);
|
|
30
30
|
return {
|
|
31
31
|
targetKind: 'WORKSPACE',
|
|
32
32
|
compatibilityMode: 'MANAGED_COPY',
|
|
@@ -4,6 +4,7 @@ import { promisify } from 'node:util';
|
|
|
4
4
|
import { lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { skillInstallMetadataSchema } from '@myagentroam/protocol';
|
|
7
|
+
import { GitCommandError, resolveWorkspaceGitRoot } from '../workspace.js';
|
|
7
8
|
import { rewriteWorkspaceGitExclude, updateWorkspaceSkillGitExclude } from './workspace-git-exclude-service.js';
|
|
8
9
|
export class SkillInstallService {
|
|
9
10
|
async recoverNodeHome(home) {
|
|
@@ -59,15 +60,15 @@ export class SkillInstallService {
|
|
|
59
60
|
await mkdir(claudeRoot, { recursive: true });
|
|
60
61
|
await assertContainedDirectory(workspace, agentsRoot);
|
|
61
62
|
await assertContainedDirectory(workspace, claudeRoot);
|
|
62
|
-
await replacePair(agentsRoot, claudeRoot, name, bundle, metadata, () => updateWorkspaceSkillGitExclude(workspace, name, true), () => updateWorkspaceSkillGitExclude(workspace, name, false));
|
|
63
|
+
await replacePair(agentsRoot, claudeRoot, name, bundle, metadata, () => updateWorkspaceSkillGitExclude(workspace, name, true, options.includeParentRepository ?? true), () => updateWorkspaceSkillGitExclude(workspace, name, false, options.includeParentRepository ?? true));
|
|
63
64
|
}
|
|
64
|
-
async removeWorkspace(workspace, name) {
|
|
65
|
+
async removeWorkspace(workspace, name, includeParentRepository = true) {
|
|
65
66
|
assertName(name);
|
|
66
67
|
const agentsRoot = path.join(workspace, '.agents', 'skills');
|
|
67
68
|
const claudeRoot = path.join(workspace, '.claude', 'skills');
|
|
68
69
|
await assertExistingContainedDirectory(workspace, agentsRoot);
|
|
69
70
|
await assertExistingContainedDirectory(workspace, claudeRoot);
|
|
70
|
-
await removePair(agentsRoot, claudeRoot, name, () => updateWorkspaceSkillGitExclude(workspace, name, false), () => updateWorkspaceSkillGitExclude(workspace, name, true));
|
|
71
|
+
await removePair(agentsRoot, claudeRoot, name, () => updateWorkspaceSkillGitExclude(workspace, name, false, includeParentRepository), () => updateWorkspaceSkillGitExclude(workspace, name, true, includeParentRepository));
|
|
71
72
|
}
|
|
72
73
|
async removeNodeHome(home, name) {
|
|
73
74
|
assertName(name);
|
|
@@ -81,7 +82,7 @@ export class SkillInstallService {
|
|
|
81
82
|
else
|
|
82
83
|
await removeSingle(agentsRoot, name);
|
|
83
84
|
}
|
|
84
|
-
async repairWorkspaceGitExclude(workspace) {
|
|
85
|
+
async repairWorkspaceGitExclude(workspace, includeParentRepository = true) {
|
|
85
86
|
const root = path.join(workspace, '.agents', 'skills');
|
|
86
87
|
const names = new Set();
|
|
87
88
|
try {
|
|
@@ -101,13 +102,20 @@ export class SkillInstallService {
|
|
|
101
102
|
if (!isMissing(error))
|
|
102
103
|
throw error;
|
|
103
104
|
}
|
|
104
|
-
await rewriteWorkspaceGitExclude(workspace, names);
|
|
105
|
+
await rewriteWorkspaceGitExclude(workspace, names, includeParentRepository);
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
export async function workspaceSkillTracked(workspace, name) {
|
|
108
109
|
assertName(name);
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
try {
|
|
111
|
+
await resolveWorkspaceGitRoot(workspace);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if ((error instanceof GitCommandError && error.code === 128) ||
|
|
115
|
+
error.code === 'ENOENT')
|
|
116
|
+
return false;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
111
119
|
try {
|
|
112
120
|
const result = await promisify(execFile)('git', ['-C', workspace, 'ls-files', '--', `.agents/skills/${name}`, `.claude/skills/${name}`], { timeout: 5_000, windowsHide: true });
|
|
113
121
|
return result.stdout.trim() !== '';
|
|
@@ -28,14 +28,14 @@ export class SkillNodeOperationService {
|
|
|
28
28
|
'workspace.skills.inspect': async (data) => {
|
|
29
29
|
const workspace = this.workspacePath(record(data)['workspaceId']);
|
|
30
30
|
await this.installer.recoverWorkspace(workspace);
|
|
31
|
-
return this.directories.inspectWorkspace(workspace);
|
|
31
|
+
return this.directories.inspectWorkspace(workspace, this.database().getWorkspaceByPath(workspace)?.includeParentRepository ?? true);
|
|
32
32
|
},
|
|
33
33
|
'workspace.skills.install': (data) => this.install('WORKSPACE', data),
|
|
34
34
|
'workspace.skills.remove': (data) => this.remove('WORKSPACE', data),
|
|
35
35
|
'workspace.skills.git-exclude-repair': async (data) => {
|
|
36
36
|
const workspace = this.workspacePath(record(data)['workspaceId']);
|
|
37
37
|
await this.installer.recoverWorkspace(workspace);
|
|
38
|
-
await this.installer.repairWorkspaceGitExclude(workspace);
|
|
38
|
+
await this.installer.repairWorkspaceGitExclude(workspace, this.database().getWorkspaceByPath(workspace)?.includeParentRepository ?? true);
|
|
39
39
|
return { repaired: true };
|
|
40
40
|
},
|
|
41
41
|
'skill.install.job.get': (data) => this.job(data)
|
|
@@ -113,7 +113,8 @@ export class SkillNodeOperationService {
|
|
|
113
113
|
await this.installer.installNodeHome(homedir(), input.name, bundle, input.metadata);
|
|
114
114
|
else
|
|
115
115
|
await this.installer.installWorkspace(input.workspace, input.name, bundle, input.metadata, {
|
|
116
|
-
confirmTracked: true
|
|
116
|
+
confirmTracked: true,
|
|
117
|
+
includeParentRepository: this.database().getWorkspaceByPath(input.workspace)?.includeParentRepository ?? true
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
120
|
async remove(target, data) {
|
|
@@ -131,7 +132,7 @@ export class SkillNodeOperationService {
|
|
|
131
132
|
}
|
|
132
133
|
else {
|
|
133
134
|
await this.installer.recoverWorkspace(workspace);
|
|
134
|
-
await this.installer.removeWorkspace(workspace, name);
|
|
135
|
+
await this.installer.removeWorkspace(workspace, name, this.database().getWorkspaceByPath(workspace)?.includeParentRepository ?? true);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
finally {
|
|
@@ -2,7 +2,7 @@ import { lstat } from 'node:fs/promises';
|
|
|
2
2
|
import { basename, posix, resolve } from 'node:path';
|
|
3
3
|
import { publicErrorMessage } from '@myagentroam/protocol';
|
|
4
4
|
import { nodeLog } from '../operational.js';
|
|
5
|
-
import { GitCommandError, isWorkspaceChangeVisible, listGitWorktrees, listGitSubmodules, readCurrentChangesSummary, resolveGitRepository } from '../workspace.js';
|
|
5
|
+
import { GitCommandError, isWorkspaceChangeVisible, listGitWorktrees, listGitSubmodules, readCurrentChangesSummary, resolveGitRepository, resolveWorkspaceGitRoot } from '../workspace.js';
|
|
6
6
|
const CACHE_TTL_MS = 5_000;
|
|
7
7
|
const REPOSITORY_READ_CONCURRENCY = 4;
|
|
8
8
|
const WORKTREE_READ_CONCURRENCY = 4;
|
|
@@ -13,6 +13,9 @@ export class WorkspaceChangeService {
|
|
|
13
13
|
constructor(state) {
|
|
14
14
|
this.state = state;
|
|
15
15
|
}
|
|
16
|
+
revision(workspaceId) {
|
|
17
|
+
return this.state.changesVersions.get(workspaceId) ?? 0;
|
|
18
|
+
}
|
|
16
19
|
invalidate(workspaceId) {
|
|
17
20
|
this.state.changesReads.delete(workspaceId);
|
|
18
21
|
this.state.changesCache.delete(workspaceId);
|
|
@@ -34,7 +37,9 @@ export class WorkspaceChangeService {
|
|
|
34
37
|
return existing;
|
|
35
38
|
// Invalidation may arrive during this shared read; its old result must not repopulate the cache.
|
|
36
39
|
const version = this.state.changesVersions.get(workspace.id) ?? 0;
|
|
37
|
-
const request = this.state.changesLimiter
|
|
40
|
+
const request = this.state.changesLimiter
|
|
41
|
+
.run(async () => {
|
|
42
|
+
const repositoryRoot = await resolveWorkspaceGitRoot(workspace.path, workspace.includeParentRepository ?? true);
|
|
38
43
|
const startedAt = performance.now();
|
|
39
44
|
const errors = [];
|
|
40
45
|
const failure = (code, error) => ({
|
|
@@ -66,7 +71,7 @@ export class WorkspaceChangeService {
|
|
|
66
71
|
};
|
|
67
72
|
const readRepository = async (repositoryPath, repositoryOverride) => {
|
|
68
73
|
const repository = repositoryOverride ??
|
|
69
|
-
(await resolveGitRepository(
|
|
74
|
+
(await resolveGitRepository(repositoryRoot, repositoryPath, [repositoryPath]));
|
|
70
75
|
if (repositoryPath !== '' || repositoryOverride !== undefined) {
|
|
71
76
|
const marker = await lstat(resolve(repository, '.git'));
|
|
72
77
|
if (marker.isSymbolicLink() || (!marker.isFile() && !marker.isDirectory()))
|
|
@@ -89,19 +94,14 @@ export class WorkspaceChangeService {
|
|
|
89
94
|
changes
|
|
90
95
|
};
|
|
91
96
|
};
|
|
92
|
-
const root = await readRepository('')
|
|
93
|
-
if (error?.code === 'ENOENT' &&
|
|
94
|
-
error.syscall?.startsWith('spawn') === true)
|
|
95
|
-
throw new Error('GIT_NOT_FOUND');
|
|
96
|
-
throw error;
|
|
97
|
-
});
|
|
97
|
+
const root = await readRepository('');
|
|
98
98
|
const children = [];
|
|
99
99
|
let parents = [''];
|
|
100
100
|
let submodulesLimited = false;
|
|
101
101
|
while (parents.length > 0 && !submodulesLimited) {
|
|
102
102
|
const discovered = await mapWithConcurrency(parents, REPOSITORY_READ_CONCURRENCY, async (parent) => {
|
|
103
103
|
try {
|
|
104
|
-
const path = await resolveGitRepository(
|
|
104
|
+
const path = await resolveGitRepository(repositoryRoot, parent, [parent]);
|
|
105
105
|
return (await listGitSubmodules(path)).map((entry) => ({
|
|
106
106
|
...entry,
|
|
107
107
|
path: parent === '' ? entry.path : `${parent}/${entry.path}`
|
|
@@ -152,7 +152,7 @@ export class WorkspaceChangeService {
|
|
|
152
152
|
.map((entry) => entry.repositoryPath);
|
|
153
153
|
}
|
|
154
154
|
children.sort((left, right) => left.repositoryPath.localeCompare(right.repositoryPath));
|
|
155
|
-
const worktreeList = await listGitWorktrees(
|
|
155
|
+
const worktreeList = await listGitWorktrees(repositoryRoot).catch((error) => {
|
|
156
156
|
discoveryFailure('WORKTREE', '', 'GIT_WORKTREE_DISCOVERY_FAILED', error);
|
|
157
157
|
return { entries: [], truncated: false };
|
|
158
158
|
});
|
|
@@ -229,6 +229,13 @@ export class WorkspaceChangeService {
|
|
|
229
229
|
diffAvailable: true
|
|
230
230
|
});
|
|
231
231
|
const value = {
|
|
232
|
+
repositoryRoot,
|
|
233
|
+
workspace: {
|
|
234
|
+
id: workspace.id,
|
|
235
|
+
path: workspace.path,
|
|
236
|
+
kind: 'GIT_WORKSPACE',
|
|
237
|
+
gitSummary: { branch: root.branch, repositoryRoot }
|
|
238
|
+
},
|
|
232
239
|
partial: errors.length > 0 ||
|
|
233
240
|
children.some((group) => group.status === 'UNAVAILABLE') ||
|
|
234
241
|
linkedRepositories.some((group) => group.status === 'UNAVAILABLE') ||
|
|
@@ -240,7 +247,7 @@ export class WorkspaceChangeService {
|
|
|
240
247
|
{
|
|
241
248
|
...root,
|
|
242
249
|
source: 'ROOT',
|
|
243
|
-
physicalPath: currentWorktree?.path ??
|
|
250
|
+
physicalPath: currentWorktree?.path ?? repositoryRoot,
|
|
244
251
|
current: true,
|
|
245
252
|
...(currentWorktree === undefined ? {} : { head: currentWorktree.head }),
|
|
246
253
|
status: root.dirty ? 'DIRTY' : 'CLEAN',
|
|
@@ -267,8 +274,17 @@ export class WorkspaceChangeService {
|
|
|
267
274
|
durationMs: Math.round(performance.now() - startedAt)
|
|
268
275
|
});
|
|
269
276
|
if ((this.state.changesVersions.get(workspace.id) ?? 0) === version)
|
|
270
|
-
this.state.changesCache.set(workspace.id, {
|
|
277
|
+
this.state.changesCache.set(workspace.id, {
|
|
278
|
+
value,
|
|
279
|
+
expiresAt: Date.now() + CACHE_TTL_MS
|
|
280
|
+
});
|
|
271
281
|
return value;
|
|
282
|
+
})
|
|
283
|
+
.catch((error) => {
|
|
284
|
+
if (error?.code === 'ENOENT' &&
|
|
285
|
+
error.syscall?.startsWith('spawn') === true)
|
|
286
|
+
throw new Error('GIT_NOT_FOUND');
|
|
287
|
+
throw error;
|
|
272
288
|
});
|
|
273
289
|
this.state.changesReads.set(workspace.id, request);
|
|
274
290
|
try {
|
|
@@ -176,7 +176,9 @@ export class WorkspaceFileService {
|
|
|
176
176
|
return { size: metadata.size, revision: metadata.sourceRevision };
|
|
177
177
|
}
|
|
178
178
|
async write(workspace, input) {
|
|
179
|
-
if (
|
|
179
|
+
if (input.repositoryPath !== undefined ||
|
|
180
|
+
input.worktreePath !== undefined ||
|
|
181
|
+
typeof input.path !== 'string' ||
|
|
180
182
|
typeof input.content !== 'string' ||
|
|
181
183
|
typeof input.expectedRevision !== 'string')
|
|
182
184
|
throw new Error('FILE_WRITE_INVALID');
|
|
@@ -1,53 +1,91 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { lstat, mkdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { GitCommandError, resolveWorkspaceGitRoot } from '../workspace.js';
|
|
6
7
|
const MAR_TEMP_DIRECTORY = '/.mar/';
|
|
7
8
|
const BLOCK_BEGIN = '# BEGIN MyAgentRoam';
|
|
8
9
|
const BLOCK_END = '# END MyAgentRoam';
|
|
9
10
|
const LEGACY_BLOCK_BEGIN = '# BEGIN MyAgentRoam Skills';
|
|
10
11
|
const LEGACY_BLOCK_END = '# END MyAgentRoam Skills';
|
|
11
|
-
export async function ensureWorkspaceGitExclude(workspace) {
|
|
12
|
-
await mutateGitExclude(workspace, () => undefined);
|
|
12
|
+
export async function ensureWorkspaceGitExclude(workspace, includeParentRepository = true) {
|
|
13
|
+
await mutateGitExclude(workspace, () => undefined, includeParentRepository);
|
|
13
14
|
}
|
|
14
|
-
export async function updateWorkspaceSkillGitExclude(workspace, name, add) {
|
|
15
|
-
await mutateGitExclude(workspace, (entries) => {
|
|
15
|
+
export async function updateWorkspaceSkillGitExclude(workspace, name, add, includeParentRepository = true) {
|
|
16
|
+
await mutateGitExclude(workspace, (entries, prefix) => {
|
|
16
17
|
for (const root of ['.agents', '.claude']) {
|
|
17
|
-
const line =
|
|
18
|
+
const line = `${prefix}${root}/skills/${name}/`;
|
|
18
19
|
if (add)
|
|
19
20
|
entries.add(line);
|
|
20
21
|
else
|
|
21
22
|
entries.delete(line);
|
|
22
23
|
}
|
|
23
|
-
});
|
|
24
|
+
}, includeParentRepository);
|
|
24
25
|
}
|
|
25
|
-
export async function rewriteWorkspaceGitExclude(workspace, names) {
|
|
26
|
-
await mutateGitExclude(workspace, (entries) => {
|
|
27
|
-
entries
|
|
26
|
+
export async function rewriteWorkspaceGitExclude(workspace, names, includeParentRepository = true) {
|
|
27
|
+
await mutateGitExclude(workspace, (entries, prefix) => {
|
|
28
|
+
for (const entry of entries)
|
|
29
|
+
if (['.agents', '.claude'].some((root) => entry.startsWith(`${prefix}${root}/skills/`)))
|
|
30
|
+
entries.delete(entry);
|
|
28
31
|
for (const name of names)
|
|
29
32
|
for (const root of ['.agents', '.claude'])
|
|
30
|
-
entries.add(
|
|
31
|
-
});
|
|
33
|
+
entries.add(`${prefix}${root}/skills/${name}/`);
|
|
34
|
+
}, includeParentRepository);
|
|
32
35
|
}
|
|
33
|
-
export async function hasBrokenWorkspaceSkillGitExclude(workspace, names) {
|
|
34
|
-
if (!(await exists(path.join(workspace, '.git'))))
|
|
35
|
-
return false;
|
|
36
|
+
export async function hasBrokenWorkspaceSkillGitExclude(workspace, names, includeParentRepository = true) {
|
|
36
37
|
try {
|
|
37
|
-
const
|
|
38
|
+
const target = await gitExcludeTarget(workspace, includeParentRepository);
|
|
39
|
+
if (target === undefined)
|
|
40
|
+
return false;
|
|
41
|
+
const { exclude, prefix } = target;
|
|
38
42
|
const content = await readFile(exclude, 'utf8');
|
|
39
43
|
return names
|
|
40
|
-
.flatMap((name) => [
|
|
44
|
+
.flatMap((name) => [`${prefix}.agents/skills/${name}/`, `${prefix}.claude/skills/${name}/`])
|
|
41
45
|
.some((entry) => !content.includes(entry));
|
|
42
46
|
}
|
|
43
47
|
catch {
|
|
44
48
|
return true;
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
const mutationTails = new Map();
|
|
52
|
+
async function gitExcludeTarget(workspace, includeParentRepository) {
|
|
53
|
+
let root;
|
|
54
|
+
try {
|
|
55
|
+
root = await resolveWorkspaceGitRoot(workspace, includeParentRepository);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if ((error instanceof GitCommandError && error.code === 128) ||
|
|
59
|
+
(error instanceof Error && error.message === 'WORKSPACE_NOT_GIT') ||
|
|
60
|
+
error.code === 'ENOENT')
|
|
61
|
+
return undefined;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
const relative = path
|
|
65
|
+
.relative(root, await realpath(workspace))
|
|
66
|
+
.split(path.sep)
|
|
67
|
+
.join('/');
|
|
68
|
+
const escaped = relative.replace(/[\\*?\[\]#! ]/g, '\\$&');
|
|
69
|
+
return { exclude: await resolveGitExclude(root), prefix: relative === '' ? '/' : `/${escaped}/` };
|
|
70
|
+
}
|
|
71
|
+
async function mutateGitExclude(workspace, mutate, includeParentRepository) {
|
|
72
|
+
const target = await gitExcludeTarget(workspace, includeParentRepository);
|
|
73
|
+
if (target === undefined)
|
|
49
74
|
return;
|
|
50
|
-
const exclude =
|
|
75
|
+
const { exclude, prefix } = target;
|
|
76
|
+
const request = (mutationTails.get(exclude) ?? Promise.resolve())
|
|
77
|
+
.catch(() => undefined)
|
|
78
|
+
.then(() => writeGitExclude(exclude, (entries) => mutate(entries, prefix)));
|
|
79
|
+
mutationTails.set(exclude, request);
|
|
80
|
+
try {
|
|
81
|
+
await request;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
if (mutationTails.get(exclude) === request)
|
|
85
|
+
mutationTails.delete(exclude);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function writeGitExclude(exclude, mutate) {
|
|
51
89
|
await mkdir(path.dirname(exclude), { recursive: true });
|
|
52
90
|
let content = '';
|
|
53
91
|
try {
|
|
@@ -62,7 +100,7 @@ async function mutateGitExclude(workspace, mutate) {
|
|
|
62
100
|
}
|
|
63
101
|
const pattern = managedBlockPattern();
|
|
64
102
|
const existing = [...content.matchAll(pattern)].map((match) => match[0]).join('\n');
|
|
65
|
-
const entries = new Set(existing.split(/\r?\n/u).filter((line) => line.startsWith('
|
|
103
|
+
const entries = new Set(existing.split(/\r?\n/u).filter((line) => line.startsWith('/')));
|
|
66
104
|
entries.delete(MAR_TEMP_DIRECTORY);
|
|
67
105
|
mutate(entries);
|
|
68
106
|
const block = entries.size === 0 ? '' : `${BLOCK_BEGIN}\n${[...entries].sort().join('\n')}\n${BLOCK_END}\n`;
|
|
@@ -97,17 +135,6 @@ async function resolveGitExclude(workspace) {
|
|
|
97
135
|
throw new Error('WORKSPACE_SKILL_GIT_EXCLUDE_FAILED');
|
|
98
136
|
}
|
|
99
137
|
}
|
|
100
|
-
async function exists(value) {
|
|
101
|
-
try {
|
|
102
|
-
await lstat(value);
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
if (isMissing(error))
|
|
107
|
-
return false;
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
138
|
function isMissing(error) {
|
|
112
139
|
return (error instanceof Error && 'code' in error && error.code === 'ENOENT');
|
|
113
140
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { rmdir } from 'node:fs/promises';
|
|
1
|
+
import { realpath, rmdir } from 'node:fs/promises';
|
|
2
2
|
import { createWorkspaceDirectory, inspectWorkspace, listWorkspaceDirectories } from '../workspace.js';
|
|
3
3
|
import { ensureWorkspaceGitExclude } from './workspace-git-exclude-service.js';
|
|
4
4
|
import { workspaceGitSettingsSchema } from '@myagentroam/protocol';
|
|
@@ -7,11 +7,13 @@ export class WorkspaceService {
|
|
|
7
7
|
config;
|
|
8
8
|
runtime;
|
|
9
9
|
onRemoved;
|
|
10
|
-
|
|
10
|
+
onGitPolicyChanged;
|
|
11
|
+
constructor(database, config, runtime, onRemoved, onGitPolicyChanged = () => undefined) {
|
|
11
12
|
this.database = database;
|
|
12
13
|
this.config = config;
|
|
13
14
|
this.runtime = runtime;
|
|
14
15
|
this.onRemoved = onRemoved;
|
|
16
|
+
this.onGitPolicyChanged = onGitPolicyChanged;
|
|
15
17
|
}
|
|
16
18
|
operations() {
|
|
17
19
|
return {
|
|
@@ -36,14 +38,28 @@ export class WorkspaceService {
|
|
|
36
38
|
const path = typeof input.createDirectoryName === 'string'
|
|
37
39
|
? (createdPath = await createWorkspaceDirectory(input.path, input.createDirectoryName, this.config().allowedRoots))
|
|
38
40
|
: input.path;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
+
const canonicalPath = await realpath(path);
|
|
42
|
+
const current = this.database().getWorkspaceByPath(canonicalPath);
|
|
43
|
+
const includeParentRepository = current?.includeParentRepository ?? true;
|
|
44
|
+
const inspected = await inspectWorkspace(canonicalPath, this.config().allowedRoots, includeParentRepository);
|
|
45
|
+
let latest = this.database().getWorkspaceByPath(canonicalPath);
|
|
46
|
+
if (latest !== undefined &&
|
|
47
|
+
(latest.includeParentRepository ?? true) !== includeParentRepository)
|
|
48
|
+
return { workspace: latest };
|
|
49
|
+
if (inspected.kind === 'GIT_WORKSPACE') {
|
|
50
|
+
await ensureWorkspaceGitExclude(inspected.path, includeParentRepository);
|
|
51
|
+
latest = this.database().getWorkspaceByPath(canonicalPath);
|
|
52
|
+
if (latest !== undefined &&
|
|
53
|
+
(latest.includeParentRepository ?? true) !== includeParentRepository)
|
|
54
|
+
return { workspace: latest };
|
|
55
|
+
}
|
|
41
56
|
return {
|
|
42
57
|
workspace: this.database().saveWorkspace({
|
|
43
58
|
path: inspected.path,
|
|
44
59
|
kind: inspected.kind,
|
|
45
60
|
gitSummary: inspected.gitSummary ?? null,
|
|
46
|
-
preserveGitConfig: input.preserveGitConfig === true
|
|
61
|
+
preserveGitConfig: input.preserveGitConfig === true,
|
|
62
|
+
includeParentRepository
|
|
47
63
|
})
|
|
48
64
|
};
|
|
49
65
|
}
|
|
@@ -80,13 +96,22 @@ export class WorkspaceService {
|
|
|
80
96
|
if (current === undefined)
|
|
81
97
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
82
98
|
if (!update)
|
|
83
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
preserveGitConfig: current.preserveGitConfig ?? true,
|
|
101
|
+
includeParentRepository: current.includeParentRepository ?? true
|
|
102
|
+
};
|
|
84
103
|
const parsed = workspaceGitSettingsSchema.safeParse({
|
|
85
|
-
preserveGitConfig: input.preserveGitConfig
|
|
104
|
+
preserveGitConfig: input.preserveGitConfig,
|
|
105
|
+
includeParentRepository: input.includeParentRepository
|
|
86
106
|
});
|
|
87
107
|
if (!parsed.success)
|
|
88
108
|
throw new Error('GIT_CONFIGURATION_INVALID');
|
|
89
|
-
const result = this.database().setWorkspaceGitPolicy(input.workspaceId, parsed.data.preserveGitConfig);
|
|
90
|
-
|
|
109
|
+
const result = this.database().setWorkspaceGitPolicy(input.workspaceId, parsed.data.preserveGitConfig, parsed.data.includeParentRepository);
|
|
110
|
+
if (result.includeParentRepository !== current.includeParentRepository)
|
|
111
|
+
this.onGitPolicyChanged(current.id);
|
|
112
|
+
return {
|
|
113
|
+
preserveGitConfig: result.preserveGitConfig,
|
|
114
|
+
includeParentRepository: result.includeParentRepository
|
|
115
|
+
};
|
|
91
116
|
}
|
|
92
117
|
}
|
|
@@ -15,15 +15,23 @@ export class WorkspaceWatchSnapshotService {
|
|
|
15
15
|
if (watch.topics.files !== undefined)
|
|
16
16
|
this.ensureFilesRefreshTimer();
|
|
17
17
|
await Promise.all(this.subscribedTopics(watch).map(async (topic) => {
|
|
18
|
+
const revision = this.readRevision(workspace.id, topic);
|
|
18
19
|
try {
|
|
19
20
|
const payload = await this.read(workspace, topic, watch);
|
|
20
21
|
if (!this.matches(workspace.id, watch))
|
|
21
22
|
return;
|
|
23
|
+
if (revision !== this.readRevision(workspace.id, topic)) {
|
|
24
|
+
void this.invalidate(workspace.id, topic);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
22
27
|
this.signatures.set(this.viewKey(workspace.id, topic, watch), signature(payload));
|
|
23
28
|
this.deliver(workspace, watch, topic, payload);
|
|
24
29
|
}
|
|
25
30
|
catch {
|
|
26
|
-
this.
|
|
31
|
+
if (revision === this.readRevision(workspace.id, topic))
|
|
32
|
+
this.fail(workspace.id, watch, topic);
|
|
33
|
+
else
|
|
34
|
+
void this.invalidate(workspace.id, topic);
|
|
27
35
|
}
|
|
28
36
|
}));
|
|
29
37
|
}
|
|
@@ -119,7 +127,22 @@ export class WorkspaceWatchSnapshotService {
|
|
|
119
127
|
const first = subscribers[0];
|
|
120
128
|
if (first === undefined)
|
|
121
129
|
return;
|
|
122
|
-
const
|
|
130
|
+
const revision = this.readRevision(workspaceId, topic);
|
|
131
|
+
let payload;
|
|
132
|
+
try {
|
|
133
|
+
payload = await this.read(workspace, topic, first);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (revision !== this.readRevision(workspaceId, topic)) {
|
|
137
|
+
void this.invalidate(workspaceId, topic);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
if (revision !== this.readRevision(workspaceId, topic)) {
|
|
143
|
+
void this.invalidate(workspaceId, topic);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
123
146
|
const activeSubscribers = subscribers.filter((watch) => this.matches(workspaceId, watch));
|
|
124
147
|
if (activeSubscribers.length === 0)
|
|
125
148
|
return;
|
|
@@ -157,7 +180,7 @@ export class WorkspaceWatchSnapshotService {
|
|
|
157
180
|
read(workspace, topic, watch) {
|
|
158
181
|
const key = topic === 'files'
|
|
159
182
|
? `${workspace.id}:files:${JSON.stringify(watch.topics.files)}`
|
|
160
|
-
: `${workspace.id}:${topic}`;
|
|
183
|
+
: `${workspace.id}:${topic}:${this.readRevision(workspace.id, topic)}`;
|
|
161
184
|
const active = this.reads.get(key);
|
|
162
185
|
if (active !== undefined)
|
|
163
186
|
return active;
|
|
@@ -166,8 +189,12 @@ export class WorkspaceWatchSnapshotService {
|
|
|
166
189
|
: topic === 'changes'
|
|
167
190
|
? workspace.kind === 'GIT_WORKSPACE'
|
|
168
191
|
? this.options.changes.current(workspace)
|
|
169
|
-
: Promise.resolve(
|
|
170
|
-
|
|
192
|
+
: Promise.resolve(workspace.includeParentRepository === false
|
|
193
|
+
? { workspace, changes: [], repositories: [] }
|
|
194
|
+
: null)
|
|
195
|
+
: this.options.files
|
|
196
|
+
.list(workspace, watch.topics.files)
|
|
197
|
+
.then((files) => ({ files, workspace: this.options.requireWorkspace(workspace.id) }))).finally(() => {
|
|
171
198
|
if (this.reads.get(key) === read)
|
|
172
199
|
this.reads.delete(key);
|
|
173
200
|
});
|
|
@@ -181,6 +208,9 @@ export class WorkspaceWatchSnapshotService {
|
|
|
181
208
|
this.signatures.set(key, next);
|
|
182
209
|
return true;
|
|
183
210
|
}
|
|
211
|
+
readRevision(workspaceId, topic) {
|
|
212
|
+
return topic === 'changes' ? this.options.changes.revision(workspaceId) : 0;
|
|
213
|
+
}
|
|
184
214
|
viewKey(workspaceId, topic, watch) {
|
|
185
215
|
return topic === 'files'
|
|
186
216
|
? `${workspaceId}:files:${JSON.stringify(watch.topics.files)}`
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { isAbsolute, win32 } from 'node:path';
|
|
3
|
+
import { listGitHistoryRefs, normalizeFilePreviewPath, openGitHistoryFileContent, readGitHistory, readGitHistoryFileDiff, readGitHistoryFileContentRange, readGitHistoryFiles, readCurrentChangeDiff, readRestrictedDiff, resolveGitRepository, resolveGitRepositoryTarget, resolveWorkspaceGitRoot, restoreCurrentChange } from '../workspace.js';
|
|
3
4
|
import { WorkspaceCodeNavigationService } from './workspace-code-navigation-service.js';
|
|
4
5
|
import { WorkspaceContentSearchService } from './workspace-content-search-service.js';
|
|
5
6
|
import { WorkspaceWatchController } from './workspace-watch-controller.js';
|
|
@@ -98,7 +99,7 @@ export class WorkspaceWorkbenchService {
|
|
|
98
99
|
}
|
|
99
100
|
async readPredictionFile(workspaceId, path, worktreePath) {
|
|
100
101
|
const workspace = this.options.requireWorkspace(workspaceId);
|
|
101
|
-
const repository = await this.fileRepository(workspace, worktreePath ? { worktreePath } : {});
|
|
102
|
+
const repository = await this.fileRepository(workspace, worktreePath ? { worktreePath, path } : {});
|
|
102
103
|
const { readWorkspaceTextFile } = await import('../workspace.js');
|
|
103
104
|
return readWorkspaceTextFile(repository, path, 0, 64 * 1024);
|
|
104
105
|
}
|
|
@@ -180,7 +181,7 @@ export class WorkspaceWorkbenchService {
|
|
|
180
181
|
(input.worktreePath !== undefined && typeof input.worktreePath !== 'string'))
|
|
181
182
|
throw new Error('GIT_DIFF_INVALID');
|
|
182
183
|
const workspace = this.gitWorkspace(input.workspaceId);
|
|
183
|
-
const repository = await resolveGitRepositoryTarget(workspace.path, input.repositoryPath ?? '', input.worktreePath);
|
|
184
|
+
const repository = await resolveGitRepositoryTarget(await resolveWorkspaceGitRoot(workspace.path, workspace.includeParentRepository ?? true), input.repositoryPath ?? '', input.worktreePath);
|
|
184
185
|
return { diff: await readCurrentChangeDiff(repository, input.path) };
|
|
185
186
|
}
|
|
186
187
|
async restoreChange(input) {
|
|
@@ -191,7 +192,7 @@ export class WorkspaceWorkbenchService {
|
|
|
191
192
|
(input.state !== 'STAGED' && input.state !== 'UNSTAGED' && input.state !== 'UNTRACKED'))
|
|
192
193
|
throw new Error('GIT_CHANGE_RESTORE_UNAVAILABLE');
|
|
193
194
|
const workspace = this.gitWorkspace(input.workspaceId);
|
|
194
|
-
const repository = await resolveGitRepositoryTarget(workspace.path, input.repositoryPath ?? '', input.worktreePath);
|
|
195
|
+
const repository = await resolveGitRepositoryTarget(await resolveWorkspaceGitRoot(workspace.path, workspace.includeParentRepository ?? true), input.repositoryPath ?? '', input.worktreePath);
|
|
195
196
|
await restoreCurrentChange(repository, input.path, input.state);
|
|
196
197
|
this.options.invalidate(workspace.id);
|
|
197
198
|
return { restored: true };
|
|
@@ -201,14 +202,22 @@ export class WorkspaceWorkbenchService {
|
|
|
201
202
|
(input.repositoryPath !== undefined && typeof input.repositoryPath !== 'string'))
|
|
202
203
|
throw new Error('GIT_REPOSITORY_INVALID');
|
|
203
204
|
const workspace = this.gitWorkspace(input.workspaceId);
|
|
204
|
-
return resolveGitRepository(workspace.path, input.repositoryPath ?? '');
|
|
205
|
+
return resolveGitRepository(await resolveWorkspaceGitRoot(workspace.path, workspace.includeParentRepository ?? true), input.repositoryPath ?? '');
|
|
205
206
|
}
|
|
206
207
|
async fileRepository(workspace, input) {
|
|
207
|
-
if (input.worktreePath === undefined)
|
|
208
|
+
if (input.worktreePath === undefined && input.repositoryPath === undefined)
|
|
208
209
|
return workspace.path;
|
|
209
|
-
if (typeof input.worktreePath !== 'string')
|
|
210
|
+
if ((input.worktreePath !== undefined && typeof input.worktreePath !== 'string') ||
|
|
211
|
+
(input.repositoryPath !== undefined && typeof input.repositoryPath !== 'string'))
|
|
210
212
|
throw new Error('GIT_REPOSITORY_INVALID');
|
|
211
|
-
|
|
213
|
+
if (typeof input.path !== 'string' ||
|
|
214
|
+
input.path.startsWith('mar-attachment:') ||
|
|
215
|
+
input.previewDirectory !== undefined)
|
|
216
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
217
|
+
const path = normalizeFilePreviewPath(input.path);
|
|
218
|
+
if (isAbsolute(path) || win32.isAbsolute(path))
|
|
219
|
+
throw new Error('FILE_PATH_ESCAPE');
|
|
220
|
+
return resolveGitRepositoryTarget(await resolveWorkspaceGitRoot(workspace.path, workspace.includeParentRepository ?? true), input.repositoryPath ?? '', input.worktreePath);
|
|
212
221
|
}
|
|
213
222
|
refs(input) {
|
|
214
223
|
if (input.refs === undefined)
|
|
@@ -280,6 +289,9 @@ export class WorkspaceWorkbenchService {
|
|
|
280
289
|
const input = {
|
|
281
290
|
workspaceId: workspace.id,
|
|
282
291
|
path: descriptor.resource.path,
|
|
292
|
+
...(descriptor.resource.repositoryPath === undefined
|
|
293
|
+
? {}
|
|
294
|
+
: { repositoryPath: descriptor.resource.repositoryPath }),
|
|
283
295
|
...(descriptor.resource.worktreePath === undefined
|
|
284
296
|
? {}
|
|
285
297
|
: { worktreePath: descriptor.resource.worktreePath })
|
|
@@ -372,12 +384,12 @@ export class WorkspaceWorkbenchService {
|
|
|
372
384
|
typeof input.toRef !== 'string')
|
|
373
385
|
throw new Error('GIT_DIFF_INVALID');
|
|
374
386
|
const workspace = this.gitWorkspace(input.workspaceId);
|
|
375
|
-
return {
|
|
387
|
+
return {
|
|
388
|
+
diff: await readRestrictedDiff(await resolveWorkspaceGitRoot(workspace.path, workspace.includeParentRepository ?? true), input.fromRef, input.toRef)
|
|
389
|
+
};
|
|
376
390
|
}
|
|
377
391
|
gitWorkspace(workspaceId) {
|
|
378
392
|
const workspace = this.options.requireWorkspace(workspaceId);
|
|
379
|
-
if (workspace.kind !== 'GIT_WORKSPACE')
|
|
380
|
-
throw new Error('WORKSPACE_NOT_GIT');
|
|
381
393
|
return workspace;
|
|
382
394
|
}
|
|
383
395
|
}
|
package/dist/workspace.js
CHANGED
|
@@ -1187,7 +1187,7 @@ export function isWithinAllowedRoot(target, roots, platform = hostPlatform()) {
|
|
|
1187
1187
|
});
|
|
1188
1188
|
}
|
|
1189
1189
|
/** Resolving both candidate and roots rejects Linux symlinks which point outside a root. */
|
|
1190
|
-
export async function inspectWorkspace(requestedPath, allowedRoots) {
|
|
1190
|
+
export async function inspectWorkspace(requestedPath, allowedRoots, includeParentRepository = true) {
|
|
1191
1191
|
const resolvedPath = await realpath(requestedPath);
|
|
1192
1192
|
const resolvedRoots = await Promise.all(allowedRoots.map((root) => realpath(root)));
|
|
1193
1193
|
const directory = await stat(resolvedPath);
|
|
@@ -1195,16 +1195,40 @@ export async function inspectWorkspace(requestedPath, allowedRoots) {
|
|
|
1195
1195
|
throw new Error('WORKSPACE_NOT_ALLOWED');
|
|
1196
1196
|
}
|
|
1197
1197
|
try {
|
|
1198
|
-
const
|
|
1198
|
+
const repositoryRoot = await resolveWorkspaceGitRoot(resolvedPath, includeParentRepository);
|
|
1199
|
+
const summary = { ...(await readGitSummary(repositoryRoot)), repositoryRoot };
|
|
1199
1200
|
return { path: resolvedPath, kind: 'GIT_WORKSPACE', gitSummary: summary };
|
|
1200
1201
|
}
|
|
1201
1202
|
catch (error) {
|
|
1202
|
-
if ((error instanceof GitCommandError && error.code === 128) ||
|
|
1203
|
+
if ((error instanceof GitCommandError && error.code === 128) ||
|
|
1204
|
+
isGitUnavailable(error) ||
|
|
1205
|
+
(error instanceof Error && error.message === 'WORKSPACE_NOT_GIT')) {
|
|
1203
1206
|
return { path: resolvedPath, kind: 'DIRECTORY' };
|
|
1204
1207
|
}
|
|
1205
1208
|
throw error;
|
|
1206
1209
|
}
|
|
1207
1210
|
}
|
|
1211
|
+
/** Resolves the working tree without changing the registered Workspace identity. */
|
|
1212
|
+
export async function resolveWorkspaceGitRoot(workspacePath, includeParentRepository = true) {
|
|
1213
|
+
const workspace = await resolveRegisteredWorkspaceRoot(workspacePath);
|
|
1214
|
+
if (!includeParentRepository) {
|
|
1215
|
+
const marker = await lstat(resolve(workspace, '.git')).catch((error) => {
|
|
1216
|
+
if (error.code === 'ENOENT')
|
|
1217
|
+
return undefined;
|
|
1218
|
+
throw error;
|
|
1219
|
+
});
|
|
1220
|
+
if (marker === undefined ||
|
|
1221
|
+
marker.isSymbolicLink() ||
|
|
1222
|
+
(!marker.isFile() && !marker.isDirectory()))
|
|
1223
|
+
throw new Error('WORKSPACE_NOT_GIT');
|
|
1224
|
+
}
|
|
1225
|
+
const result = await runReadOnlyGit(workspace, ['rev-parse', '--show-toplevel']);
|
|
1226
|
+
const root = await realpath(result.text.replace(/\n$/u, ''));
|
|
1227
|
+
if (!includeParentRepository &&
|
|
1228
|
+
normalizeWorkspacePath(root) !== normalizeWorkspacePath(workspace))
|
|
1229
|
+
throw new Error('WORKSPACE_NOT_GIT');
|
|
1230
|
+
return root;
|
|
1231
|
+
}
|
|
1208
1232
|
export async function readGitSummary(workspacePath) {
|
|
1209
1233
|
const commonDir = (await runReadOnlyGit(workspacePath, ['rev-parse', '--git-common-dir'])).text.trim();
|
|
1210
1234
|
const headResult = await runReadOnlyGitAllowFailure(workspacePath, [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"playwright": "1.62.1",
|
|
38
38
|
"ws": "^8.21.3",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@myagentroam/agent": "0.11.
|
|
41
|
-
"@myagentroam/
|
|
40
|
+
"@myagentroam/agent": "0.11.2",
|
|
41
|
+
"@myagentroam/protocol": "0.11.2",
|
|
42
42
|
"@myagentroam/marctl": "0.11.0",
|
|
43
|
-
"@myagentroam/
|
|
43
|
+
"@myagentroam/code-navigation": "0.11.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/ws": "^8.18.1"
|