engineering-memory 1.11.15 → 1.11.16
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/package.json +1 -1
- package/runtime/build.json +1 -1
- package/runtime/dist/src/runtime/bridge-service.js +5 -5
- package/runtime/dist/src/runtime/worktree-editor.js +184 -0
- package/runtime/dist/src/runtime/worktree-gradle.js +318 -0
- package/runtime/dist/src/runtime/worktree-pool.js +181 -3
- package/runtime/dist/src/runtime/worktree-preparation.js +901 -0
- package/runtime/dist/src/runtime/worktree-readiness-types.js +18 -0
- package/skill/references/lifecycle.md +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "engineering-memory",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.16",
|
|
4
4
|
"description": "Installs the Engineering Memory skill and its local MCP bridge. Sign in after installing; your organization and project are resolved from your account.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/runtime/build.json
CHANGED
|
@@ -544,7 +544,7 @@ export class BridgeService {
|
|
|
544
544
|
if ((!sourceCommit && !(previous && previous.baseCommit === null && !previous.managed)) ||
|
|
545
545
|
branch === undefined)
|
|
546
546
|
throw refuse('The task branch and base must be restored before resuming its worktree.', 'worktree.reconcile');
|
|
547
|
-
const allocation = await pool.allocate({
|
|
547
|
+
const allocation = await pool.prepare(await pool.allocate({
|
|
548
548
|
projectId,
|
|
549
549
|
projectName: projectId,
|
|
550
550
|
repoFingerprint: repository.repoFingerprint,
|
|
@@ -554,7 +554,7 @@ export class BridgeService {
|
|
|
554
554
|
baseCommit: sourceCommit ?? null,
|
|
555
555
|
keepCurrent: previous ? !previous.managed : false,
|
|
556
556
|
resume: true,
|
|
557
|
-
}, await this.poolPolicy());
|
|
557
|
+
}, await this.poolPolicy()));
|
|
558
558
|
await pool.bind(projectId, allocation.repoRoot, allocation.generation, pointer.taskId);
|
|
559
559
|
allocation.taskId = pointer.taskId;
|
|
560
560
|
this.poolAllocations.set(allocation.repoRoot, allocation);
|
|
@@ -2615,7 +2615,7 @@ export class BridgeService {
|
|
|
2615
2615
|
const allocationTimer = startPhaseTimer('task.branch.allocate');
|
|
2616
2616
|
const allocationPolicy = await this.poolPolicy(true);
|
|
2617
2617
|
allocationTimer.mark('policy');
|
|
2618
|
-
const allocation = await pool.allocate({
|
|
2618
|
+
const allocation = await pool.prepare(await pool.allocate({
|
|
2619
2619
|
projectId,
|
|
2620
2620
|
projectName: preferences.projectName ?? projectId,
|
|
2621
2621
|
repoFingerprint: repository.repoFingerprint,
|
|
@@ -2626,7 +2626,7 @@ export class BridgeService {
|
|
|
2626
2626
|
keepCurrent: input.keepCurrent,
|
|
2627
2627
|
inPlace: !existing && input.inPlace,
|
|
2628
2628
|
resume: Boolean(existing),
|
|
2629
|
-
}, allocationPolicy);
|
|
2629
|
+
}, allocationPolicy));
|
|
2630
2630
|
allocationTimer.mark('pool_allocate');
|
|
2631
2631
|
allocationTimer.finish();
|
|
2632
2632
|
this.poolAllocations.set(allocation.repoRoot, allocation);
|
|
@@ -2648,7 +2648,7 @@ export class BridgeService {
|
|
|
2648
2648
|
return asJsonValue({
|
|
2649
2649
|
...allocation,
|
|
2650
2650
|
kept: !allocation.managed,
|
|
2651
|
-
nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot.
|
|
2651
|
+
nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot. Inspect readiness separately: a file problem does not cancel the task, and an editor request does not prove its window is visible. Resolve reported local file issues without overwriting existing files, then retry task.branch or session.resume. Use task.heartbeat during long work and pause before handoff.',
|
|
2652
2652
|
});
|
|
2653
2653
|
}
|
|
2654
2654
|
adoptOwned(repoRoot, entry) {
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { access, open, realpath, stat } from 'node:fs/promises';
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
export var WorktreeEditorStatus;
|
|
6
|
+
(function (WorktreeEditorStatus) {
|
|
7
|
+
WorktreeEditorStatus["Requested"] = "requested";
|
|
8
|
+
WorktreeEditorStatus["Unavailable"] = "unavailable";
|
|
9
|
+
WorktreeEditorStatus["Failed"] = "failed";
|
|
10
|
+
WorktreeEditorStatus["Skipped"] = "skipped";
|
|
11
|
+
})(WorktreeEditorStatus || (WorktreeEditorStatus = {}));
|
|
12
|
+
export async function openWorktreeInEditor(repoRoot) {
|
|
13
|
+
if (!['win32', 'darwin', 'linux'].includes(process.platform) ||
|
|
14
|
+
(process.env.CI && !/^(false|0)$/i.test(process.env.CI)) ||
|
|
15
|
+
process.env.SSH_CONNECTION ||
|
|
16
|
+
process.env.SSH_CLIENT ||
|
|
17
|
+
process.env.SSH_TTY ||
|
|
18
|
+
process.env.VSCODE_AGENT_FOLDER ||
|
|
19
|
+
process.env.VSCODE_REMOTE_NAME ||
|
|
20
|
+
process.env.WSL_DISTRO_NAME ||
|
|
21
|
+
process.env.CODESPACES === 'true' ||
|
|
22
|
+
process.env.SESSIONNAME?.toLowerCase() === 'services' ||
|
|
23
|
+
(process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY)) {
|
|
24
|
+
return {
|
|
25
|
+
status: WorktreeEditorStatus.Skipped,
|
|
26
|
+
detail: 'A local desktop session is not available.',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let folder;
|
|
30
|
+
try {
|
|
31
|
+
if (!repoRoot.trim())
|
|
32
|
+
throw new Error();
|
|
33
|
+
folder = await realpath(resolve(repoRoot));
|
|
34
|
+
if (!(await stat(folder)).isDirectory())
|
|
35
|
+
throw new Error();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return {
|
|
39
|
+
status: WorktreeEditorStatus.Failed,
|
|
40
|
+
detail: 'The worktree folder is not accessible.',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const command = await findEditor().catch(() => null);
|
|
44
|
+
if (!command) {
|
|
45
|
+
return {
|
|
46
|
+
status: WorktreeEditorStatus.Unavailable,
|
|
47
|
+
detail: 'An installed local VS Code CLI was not found.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (/[\\/](?:\.vscode-server(?:-insiders)?|remote-cli)[\\/]/i.test(command.executable)) {
|
|
51
|
+
return {
|
|
52
|
+
status: WorktreeEditorStatus.Skipped,
|
|
53
|
+
detail: 'The available VS Code CLI belongs to a remote session.',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const env = { ...process.env };
|
|
57
|
+
delete env.VSCODE_DEV;
|
|
58
|
+
if (process.platform === 'win32')
|
|
59
|
+
env.ELECTRON_RUN_AS_NODE = '1';
|
|
60
|
+
try {
|
|
61
|
+
return await new Promise((resolveResult) => {
|
|
62
|
+
const child = spawn(command.executable, [...command.args, '--new-window', folder], {
|
|
63
|
+
cwd: folder,
|
|
64
|
+
env,
|
|
65
|
+
shell: false,
|
|
66
|
+
windowsHide: true,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
});
|
|
69
|
+
let settled = false;
|
|
70
|
+
let outputBytes = 0;
|
|
71
|
+
const timer = setTimeout(() => stop('The VS Code request timed out; window state is unknown.'), 10_000);
|
|
72
|
+
const finish = (status, detail) => {
|
|
73
|
+
if (settled)
|
|
74
|
+
return;
|
|
75
|
+
settled = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
child.stdout.destroy();
|
|
78
|
+
child.stderr.destroy();
|
|
79
|
+
child.unref();
|
|
80
|
+
resolveResult({ status, detail });
|
|
81
|
+
};
|
|
82
|
+
const stop = (detail) => {
|
|
83
|
+
if (settled)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
child.kill('SIGKILL');
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
finish(WorktreeEditorStatus.Failed, detail);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const discardOutput = (chunk) => {
|
|
93
|
+
outputBytes += chunk.length;
|
|
94
|
+
if (outputBytes > 65_536) {
|
|
95
|
+
stop('The VS Code CLI exceeded the output limit; window state is unknown.');
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
child.stdout.on('data', discardOutput);
|
|
99
|
+
child.stderr.on('data', discardOutput);
|
|
100
|
+
child.once('error', () => finish(WorktreeEditorStatus.Failed, 'The VS Code CLI could not start.'));
|
|
101
|
+
child.once('close', (code) => finish(code === 0 ? WorktreeEditorStatus.Requested : WorktreeEditorStatus.Failed, code === 0
|
|
102
|
+
? 'VS Code launch requested; window state was not verified.'
|
|
103
|
+
: 'The VS Code CLI failed; window state is unknown.'));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return { status: WorktreeEditorStatus.Failed, detail: 'The VS Code CLI could not start.' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function findEditor() {
|
|
111
|
+
const directories = (process.env.PATH ?? '')
|
|
112
|
+
.split(delimiter)
|
|
113
|
+
.map((directory) => directory.replace(/^"(.+)"$/, '$1'))
|
|
114
|
+
.filter(isAbsolute)
|
|
115
|
+
.slice(0, 128);
|
|
116
|
+
if (process.platform === 'win32') {
|
|
117
|
+
for (const root of [
|
|
118
|
+
process.env.LOCALAPPDATA && join(process.env.LOCALAPPDATA, 'Programs'),
|
|
119
|
+
process.env.ProgramFiles,
|
|
120
|
+
process.env['ProgramFiles(x86)'],
|
|
121
|
+
]) {
|
|
122
|
+
if (root && isAbsolute(root))
|
|
123
|
+
directories.push(join(root, 'Microsoft VS Code', 'bin'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const directory of new Set(directories)) {
|
|
127
|
+
if (process.platform === 'win32') {
|
|
128
|
+
const command = await windowsEditor(directory);
|
|
129
|
+
if (command)
|
|
130
|
+
return command;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const executable = join(directory, 'code');
|
|
134
|
+
if (await executableFile(executable)) {
|
|
135
|
+
return { executable: await realpath(executable), args: [] };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
async function windowsEditor(bin) {
|
|
142
|
+
let handle;
|
|
143
|
+
try {
|
|
144
|
+
const launcher = await realpath(join(bin, 'code.cmd'));
|
|
145
|
+
handle = await open(launcher, 'r');
|
|
146
|
+
const info = await handle.stat();
|
|
147
|
+
if (!info.isFile() || info.size > 16_384)
|
|
148
|
+
return null;
|
|
149
|
+
const bytes = Buffer.alloc(16_385);
|
|
150
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
151
|
+
if (bytesRead > 16_384)
|
|
152
|
+
return null;
|
|
153
|
+
const match = bytes
|
|
154
|
+
.subarray(0, bytesRead)
|
|
155
|
+
.toString('utf8')
|
|
156
|
+
.match(/^[ \t]*"%~dp0\.\.\\Code\.exe"[ \t]+"%~dp0\.\.\\((?:[a-f0-9]{7,40}\\)?resources\\app\\out\\cli\.js)"[ \t]+%\*[ \t]*\r?$/im);
|
|
157
|
+
if (!match)
|
|
158
|
+
return null;
|
|
159
|
+
const installation = resolve(dirname(launcher), '..');
|
|
160
|
+
const executable = join(installation, 'Code.exe');
|
|
161
|
+
const cli = join(installation, match[1]);
|
|
162
|
+
if (!(await executableFile(executable)) || !(await stat(cli)).isFile())
|
|
163
|
+
return null;
|
|
164
|
+
return { executable, args: [cli] };
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await handle?.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function executableFile(path) {
|
|
174
|
+
try {
|
|
175
|
+
if (!(await stat(path)).isFile())
|
|
176
|
+
return false;
|
|
177
|
+
await access(path, process.platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=worktree-editor.js.map
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
function unsupported() {
|
|
2
|
+
throw new Error('Unsupported or ambiguous Gradle signing configuration');
|
|
3
|
+
}
|
|
4
|
+
function tokenize(text) {
|
|
5
|
+
if (text.length > 256 * 1024)
|
|
6
|
+
unsupported();
|
|
7
|
+
const tokens = [];
|
|
8
|
+
let cursor = 0;
|
|
9
|
+
while (cursor < text.length) {
|
|
10
|
+
if (/\s/.test(text[cursor])) {
|
|
11
|
+
cursor++;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (text.startsWith('//', cursor)) {
|
|
15
|
+
while (cursor < text.length && !/[\r\n]/.test(text[cursor]))
|
|
16
|
+
cursor++;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (text.startsWith('/*', cursor)) {
|
|
20
|
+
let depth = 1;
|
|
21
|
+
cursor += 2;
|
|
22
|
+
while (cursor < text.length && depth) {
|
|
23
|
+
if (text.startsWith('/*', cursor)) {
|
|
24
|
+
depth++;
|
|
25
|
+
cursor += 2;
|
|
26
|
+
}
|
|
27
|
+
else if (text.startsWith('*/', cursor)) {
|
|
28
|
+
depth--;
|
|
29
|
+
cursor += 2;
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
cursor++;
|
|
33
|
+
}
|
|
34
|
+
if (depth)
|
|
35
|
+
unsupported();
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const start = cursor;
|
|
39
|
+
const quote = text[cursor];
|
|
40
|
+
if (quote === '"' || quote === "'") {
|
|
41
|
+
if (text.startsWith(quote.repeat(3), cursor)) {
|
|
42
|
+
const end = text.indexOf(quote.repeat(3), cursor + 3);
|
|
43
|
+
if (end < 0)
|
|
44
|
+
unsupported();
|
|
45
|
+
cursor = end + 3;
|
|
46
|
+
tokens.push({ kind: 'string', value: '', literal: false, start, end: cursor });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
cursor++;
|
|
50
|
+
let value = '';
|
|
51
|
+
let literal = true;
|
|
52
|
+
while (cursor < text.length && text[cursor] !== quote) {
|
|
53
|
+
let character = text[cursor++];
|
|
54
|
+
if (/[\r\n]/.test(character))
|
|
55
|
+
unsupported();
|
|
56
|
+
if (character === '\\') {
|
|
57
|
+
character = text[cursor++] ?? '';
|
|
58
|
+
if (character === 'u') {
|
|
59
|
+
const digits = text.slice(cursor, cursor + 4);
|
|
60
|
+
if (!/^[a-fA-F0-9]{4}$/.test(digits))
|
|
61
|
+
unsupported();
|
|
62
|
+
value += String.fromCharCode(parseInt(digits, 16));
|
|
63
|
+
cursor += 4;
|
|
64
|
+
}
|
|
65
|
+
else if ('\\\'"'.includes(character) && character)
|
|
66
|
+
value += character;
|
|
67
|
+
else {
|
|
68
|
+
literal = false;
|
|
69
|
+
value += character;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
if (character === '$')
|
|
74
|
+
literal = false;
|
|
75
|
+
value += character;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (text[cursor++] !== quote)
|
|
79
|
+
unsupported();
|
|
80
|
+
tokens.push({ kind: 'string', value, literal, start, end: cursor });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const word = /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(text.slice(cursor))?.[0];
|
|
84
|
+
cursor += word?.length ?? 1;
|
|
85
|
+
tokens.push({ kind: word ? 'word' : 'symbol', value: word ?? quote, start, end: cursor });
|
|
86
|
+
}
|
|
87
|
+
return tokens;
|
|
88
|
+
}
|
|
89
|
+
export function parseGradleSigning(text) {
|
|
90
|
+
const tokens = tokenize(text);
|
|
91
|
+
const signing = tokens.flatMap((token, index) => token.kind === 'word' && ['storeFile', 'setStoreFile'].includes(token.value) ? [index] : []);
|
|
92
|
+
if (!signing.length)
|
|
93
|
+
return null;
|
|
94
|
+
const value = (index) => tokens[index]?.value;
|
|
95
|
+
const expect = (index, expected) => {
|
|
96
|
+
if (value(index) !== expected || tokens[index]?.kind === 'string')
|
|
97
|
+
unsupported();
|
|
98
|
+
return index + 1;
|
|
99
|
+
};
|
|
100
|
+
const word = (index) => {
|
|
101
|
+
if (tokens[index]?.kind !== 'word')
|
|
102
|
+
unsupported();
|
|
103
|
+
return tokens[index].value;
|
|
104
|
+
};
|
|
105
|
+
const literal = (index) => {
|
|
106
|
+
const token = tokens[index];
|
|
107
|
+
if (token?.kind !== 'string' || !token.literal || !token.value || token.value.includes('$'))
|
|
108
|
+
unsupported();
|
|
109
|
+
return token.value;
|
|
110
|
+
};
|
|
111
|
+
const scopes = [];
|
|
112
|
+
const braces = [];
|
|
113
|
+
for (const [index, token] of tokens.entries()) {
|
|
114
|
+
scopes.push(braces.at(-1) ?? -1);
|
|
115
|
+
if (token.kind !== 'symbol')
|
|
116
|
+
continue;
|
|
117
|
+
if (token.value === '{')
|
|
118
|
+
braces.push(index);
|
|
119
|
+
if (token.value === '}' && braces.pop() === undefined)
|
|
120
|
+
unsupported();
|
|
121
|
+
}
|
|
122
|
+
if (braces.length)
|
|
123
|
+
unsupported();
|
|
124
|
+
const startStatement = (index) => {
|
|
125
|
+
if (!index ||
|
|
126
|
+
(tokens[index - 1]?.kind === 'symbol' && [';', '{', '}'].includes(value(index - 1))))
|
|
127
|
+
return;
|
|
128
|
+
const previous = value(index - 1);
|
|
129
|
+
if (!/[\r\n]/.test(text.slice(tokens[index - 1].end, tokens[index].start)) ||
|
|
130
|
+
['=', '(', '[', ',', '.', '?', ':', '+', '-', '*', '/', 'else', 'do'].includes(previous))
|
|
131
|
+
unsupported();
|
|
132
|
+
if (previous === ')') {
|
|
133
|
+
let cursor = index - 1;
|
|
134
|
+
let depth = 0;
|
|
135
|
+
do {
|
|
136
|
+
if (tokens[cursor]?.kind !== 'string') {
|
|
137
|
+
if (value(cursor) === ')')
|
|
138
|
+
depth++;
|
|
139
|
+
if (value(cursor) === '(')
|
|
140
|
+
depth--;
|
|
141
|
+
}
|
|
142
|
+
cursor--;
|
|
143
|
+
} while (cursor >= 0 && depth);
|
|
144
|
+
if (['if', 'for', 'while', 'when', 'catch'].includes(value(cursor)))
|
|
145
|
+
unsupported();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const endStatement = (index) => {
|
|
149
|
+
if (index === tokens.length ||
|
|
150
|
+
(tokens[index]?.kind === 'symbol' && [';', '}'].includes(value(index))))
|
|
151
|
+
return;
|
|
152
|
+
const gap = text.slice(tokens[index - 1].end, tokens[index].start);
|
|
153
|
+
if (!/[\r\n]/.test(gap) ||
|
|
154
|
+
[
|
|
155
|
+
'.',
|
|
156
|
+
'?',
|
|
157
|
+
'+',
|
|
158
|
+
'-',
|
|
159
|
+
'*',
|
|
160
|
+
'/',
|
|
161
|
+
'%',
|
|
162
|
+
'[',
|
|
163
|
+
'(',
|
|
164
|
+
'=',
|
|
165
|
+
'&',
|
|
166
|
+
'|',
|
|
167
|
+
'^',
|
|
168
|
+
'<',
|
|
169
|
+
'>',
|
|
170
|
+
'!',
|
|
171
|
+
'{',
|
|
172
|
+
':',
|
|
173
|
+
',',
|
|
174
|
+
'as',
|
|
175
|
+
'in',
|
|
176
|
+
'instanceof',
|
|
177
|
+
].includes(value(index)))
|
|
178
|
+
unsupported();
|
|
179
|
+
};
|
|
180
|
+
const fileCall = (index) => {
|
|
181
|
+
word(index);
|
|
182
|
+
const root = value(index) === 'rootProject';
|
|
183
|
+
if (root || value(index) === 'project')
|
|
184
|
+
index = expect(index + 1, '.');
|
|
185
|
+
index = expect(index, 'file');
|
|
186
|
+
return { index: expect(index, '('), root };
|
|
187
|
+
};
|
|
188
|
+
const access = (index) => {
|
|
189
|
+
if (value(index) === '[') {
|
|
190
|
+
const key = literal(index + 1);
|
|
191
|
+
return { key, next: expect(index + 2, ']') };
|
|
192
|
+
}
|
|
193
|
+
index = expect(index, '.');
|
|
194
|
+
index = expect(index, 'getProperty');
|
|
195
|
+
index = expect(index, '(');
|
|
196
|
+
const key = literal(index++);
|
|
197
|
+
return { key, next: expect(index, ')') };
|
|
198
|
+
};
|
|
199
|
+
const declaration = (name, type) => {
|
|
200
|
+
const matches = tokens.flatMap((token, index) => {
|
|
201
|
+
if (token.kind !== 'word' ||
|
|
202
|
+
token.value !== name ||
|
|
203
|
+
tokens[index - 1]?.kind !== 'word' ||
|
|
204
|
+
!['def', 'val', 'var', type].includes(value(index - 1)))
|
|
205
|
+
return [];
|
|
206
|
+
if (scopes[index] !== -1)
|
|
207
|
+
unsupported();
|
|
208
|
+
startStatement(index - 1);
|
|
209
|
+
let next = index + 1;
|
|
210
|
+
if (value(next) === ':')
|
|
211
|
+
next = expect(next + 1, type);
|
|
212
|
+
return [{ index, next: expect(next, '=') }];
|
|
213
|
+
});
|
|
214
|
+
if (matches.length !== 1)
|
|
215
|
+
unsupported();
|
|
216
|
+
return matches[0];
|
|
217
|
+
};
|
|
218
|
+
let propertiesName;
|
|
219
|
+
let keyRoot = false;
|
|
220
|
+
const signingReads = new Set();
|
|
221
|
+
for (const start of signing) {
|
|
222
|
+
startStatement(start);
|
|
223
|
+
if (value(start) !== 'storeFile' || ['.', 'def', 'val', 'var'].includes(value(start - 1)))
|
|
224
|
+
unsupported();
|
|
225
|
+
let index = start + 1;
|
|
226
|
+
if (value(index) === '=')
|
|
227
|
+
index++;
|
|
228
|
+
const call = fileCall(index);
|
|
229
|
+
index = call.index;
|
|
230
|
+
const name = word(index);
|
|
231
|
+
signingReads.add(index++);
|
|
232
|
+
if (propertiesName && (propertiesName !== name || keyRoot !== call.root))
|
|
233
|
+
unsupported();
|
|
234
|
+
propertiesName = name;
|
|
235
|
+
keyRoot = call.root;
|
|
236
|
+
const read = access(index);
|
|
237
|
+
if (read.key !== 'storeFile')
|
|
238
|
+
unsupported();
|
|
239
|
+
index = read.next;
|
|
240
|
+
if (value(index) === 'as')
|
|
241
|
+
index = expect(index + 1, 'String');
|
|
242
|
+
endStatement(expect(index, ')'));
|
|
243
|
+
}
|
|
244
|
+
const properties = declaration(propertiesName, 'Properties');
|
|
245
|
+
let index = properties.next;
|
|
246
|
+
if (value(index) === 'new')
|
|
247
|
+
index++;
|
|
248
|
+
index = expect(index, 'Properties');
|
|
249
|
+
index = expect(index, '(');
|
|
250
|
+
endStatement(expect(index, ')'));
|
|
251
|
+
const loads = [];
|
|
252
|
+
for (const [position, token] of tokens.entries()) {
|
|
253
|
+
if (token.kind !== 'word' ||
|
|
254
|
+
token.value !== propertiesName ||
|
|
255
|
+
position === properties.index ||
|
|
256
|
+
signingReads.has(position))
|
|
257
|
+
continue;
|
|
258
|
+
if (value(position - 1) === '.')
|
|
259
|
+
unsupported();
|
|
260
|
+
if (value(position + 1) === '.' && value(position + 2) === 'load') {
|
|
261
|
+
let next = expect(position + 3, '(');
|
|
262
|
+
if (value(next) === 'new')
|
|
263
|
+
next++;
|
|
264
|
+
next = expect(next, 'FileInputStream');
|
|
265
|
+
next = expect(next, '(');
|
|
266
|
+
const fileIndex = next;
|
|
267
|
+
const fileName = word(next++);
|
|
268
|
+
next = expect(next, ')');
|
|
269
|
+
endStatement(expect(next, ')'));
|
|
270
|
+
loads.push({ index: position, fileIndex, fileName });
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
const read = access(position + 1);
|
|
274
|
+
if (tokens[read.next]?.kind === 'symbol' &&
|
|
275
|
+
![')', ']', '}', ',', ';'].includes(value(read.next)))
|
|
276
|
+
unsupported();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (loads.length !== 1)
|
|
280
|
+
unsupported();
|
|
281
|
+
const load = loads[0];
|
|
282
|
+
startStatement(load.index);
|
|
283
|
+
const scope = scopes[load.index];
|
|
284
|
+
if (scope !== -1) {
|
|
285
|
+
if (scopes[scope] !== -1)
|
|
286
|
+
unsupported();
|
|
287
|
+
let guard = scope - 8;
|
|
288
|
+
if (guard < 0)
|
|
289
|
+
unsupported();
|
|
290
|
+
startStatement(guard);
|
|
291
|
+
for (const expected of ['if', '(', load.fileName, '.', 'exists', '(', ')', ')']) {
|
|
292
|
+
guard = expect(guard, expected);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (properties.index >= load.index || signing.some((position) => position <= load.index))
|
|
296
|
+
unsupported();
|
|
297
|
+
const source = declaration(load.fileName, 'File');
|
|
298
|
+
if (source.index >= load.index)
|
|
299
|
+
unsupported();
|
|
300
|
+
const call = fileCall(source.next);
|
|
301
|
+
const propertiesPath = literal(call.index);
|
|
302
|
+
endStatement(expect(call.index + 1, ')'));
|
|
303
|
+
for (const [position, token] of tokens.entries()) {
|
|
304
|
+
if (token.kind !== 'word' ||
|
|
305
|
+
token.value !== load.fileName ||
|
|
306
|
+
position === source.index ||
|
|
307
|
+
position === load.fileIndex)
|
|
308
|
+
continue;
|
|
309
|
+
if (value(position - 1) === '.')
|
|
310
|
+
unsupported();
|
|
311
|
+
let next = expect(position + 1, '.');
|
|
312
|
+
next = expect(next, 'exists');
|
|
313
|
+
next = expect(next, '(');
|
|
314
|
+
expect(next, ')');
|
|
315
|
+
}
|
|
316
|
+
return { propertiesPath, propertiesRoot: call.root, keyRoot };
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=worktree-gradle.js.map
|