agent-state-machine 2.1.8 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -5
- package/bin/cli.js +1 -140
- package/lib/config-utils.js +259 -0
- package/lib/file-tree.js +366 -0
- package/lib/index.js +109 -2
- package/lib/llm.js +35 -5
- package/lib/runtime/agent.js +146 -118
- package/lib/runtime/model-resolution.js +128 -0
- package/lib/runtime/runtime.js +13 -2
- package/lib/runtime/track-changes.js +252 -0
- package/package.json +1 -1
- package/templates/project-builder/agents/assumptions-clarifier.md +0 -8
- package/templates/project-builder/agents/code-reviewer.md +0 -8
- package/templates/project-builder/agents/code-writer.md +0 -10
- package/templates/project-builder/agents/requirements-clarifier.md +0 -7
- package/templates/project-builder/agents/roadmap-generator.md +0 -10
- package/templates/project-builder/agents/scope-clarifier.md +0 -6
- package/templates/project-builder/agents/security-clarifier.md +0 -9
- package/templates/project-builder/agents/security-reviewer.md +0 -12
- package/templates/project-builder/agents/task-planner.md +0 -10
- package/templates/project-builder/agents/test-planner.md +0 -9
- package/templates/project-builder/config.js +13 -1
- package/templates/starter/config.js +12 -1
- package/vercel-server/public/remote/assets/{index-Cunx4VJE.js → index-BOKpYANC.js} +32 -27
- package/vercel-server/public/remote/assets/index-DHL_iHQW.css +1 -0
- package/vercel-server/public/remote/index.html +2 -2
- package/vercel-server/ui/src/components/ContentCard.jsx +6 -6
- package/vercel-server/public/remote/assets/index-D02x57pS.css +0 -1
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@ A workflow runner for building **linear, stateful agent workflows** in plain Jav
|
|
|
4
4
|
|
|
5
5
|
You write normal `async/await` code. The runtime handles:
|
|
6
6
|
- **Auto-persisted** `memory` (saved to disk on mutation)
|
|
7
|
+
- **Auto-tracked** `fileTree` (detects file changes made by agents via Git)
|
|
7
8
|
- **Human-in-the-loop** blocking via `askHuman()` or agent-driven interactions
|
|
8
9
|
- Local **JS agents** + **Markdown agents** (LLM-powered)
|
|
9
10
|
- **Agent retries** with history logging for failures
|
|
@@ -26,7 +27,7 @@ pnpm add -g agent-state-machine
|
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
### Local Library
|
|
29
|
-
Required so your `workflow.js` can `import { agent, memory } from 'agent-state-machine'`.
|
|
30
|
+
Required so your `workflow.js` can `import { agent, memory, fileTree } from 'agent-state-machine'`.
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
33
|
# npm
|
|
@@ -163,12 +164,56 @@ Context is explicit: only `params` are provided to agents unless you pass additi
|
|
|
163
164
|
A persisted object for your workflow.
|
|
164
165
|
|
|
165
166
|
- Mutations auto-save to `workflows/<name>/state/current.json`.
|
|
166
|
-
- Use it as your
|
|
167
|
+
- Use it as your "long-lived state" between runs.
|
|
167
168
|
|
|
168
169
|
```js
|
|
169
170
|
memory.count = (memory.count || 0) + 1;
|
|
170
171
|
```
|
|
171
172
|
|
|
173
|
+
### `fileTree`
|
|
174
|
+
|
|
175
|
+
Auto-tracked file changes made by agents.
|
|
176
|
+
|
|
177
|
+
- Before each `await agent(...)`, the runtime captures a Git baseline
|
|
178
|
+
- After the agent completes, it detects created/modified/deleted files
|
|
179
|
+
- Changes are stored in `memory.fileTree` and persisted to `current.json`
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
// Files are auto-tracked when agents create them
|
|
183
|
+
await agent('code-writer', { task: 'Create auth module' });
|
|
184
|
+
|
|
185
|
+
// Access tracked files
|
|
186
|
+
console.log(memory.fileTree);
|
|
187
|
+
// { "src/auth.js": { status: "created", createdBy: "code-writer", ... } }
|
|
188
|
+
|
|
189
|
+
// Pass file context to other agents
|
|
190
|
+
await agent('code-reviewer', { fileTree: memory.fileTree });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Configuration in `config.js`:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
export const config = {
|
|
197
|
+
// ... models and apiKeys ...
|
|
198
|
+
projectRoot: process.env.PROJECT_ROOT, // defaults to ../.. from workflow
|
|
199
|
+
fileTracking: true, // enable/disable (default: true)
|
|
200
|
+
fileTrackingIgnore: ['node_modules/**', '.git/**', 'dist/**'],
|
|
201
|
+
fileTrackingKeepDeleted: false // keep deleted files in tree
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### `trackFile(path, options?)` / `untrackFile(path)`
|
|
206
|
+
|
|
207
|
+
Manual file tracking utilities:
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
import { trackFile, getFileTree, untrackFile } from 'agent-state-machine';
|
|
211
|
+
|
|
212
|
+
trackFile('README.md', { caption: 'Project docs' });
|
|
213
|
+
const tree = getFileTree();
|
|
214
|
+
untrackFile('old-file.js');
|
|
215
|
+
```
|
|
216
|
+
|
|
172
217
|
### `askHuman(question, options?)`
|
|
173
218
|
|
|
174
219
|
Gets user input.
|
|
@@ -217,8 +262,13 @@ export default async function handler(context) {
|
|
|
217
262
|
// context includes:
|
|
218
263
|
// - params passed to agent(name, params)
|
|
219
264
|
// - context._steering (global + optional additional steering content)
|
|
220
|
-
// - context._config (models/apiKeys/workflowDir)
|
|
221
|
-
|
|
265
|
+
// - context._config (models/apiKeys/workflowDir/projectRoot)
|
|
266
|
+
|
|
267
|
+
// Optionally return _files to annotate tracked files
|
|
268
|
+
return {
|
|
269
|
+
ok: true,
|
|
270
|
+
_files: [{ path: 'src/example.js', caption: 'Example module' }]
|
|
271
|
+
};
|
|
222
272
|
}
|
|
223
273
|
```
|
|
224
274
|
|
|
@@ -308,7 +358,7 @@ The runtime captures the fully-built prompt in `state/history.jsonl`, viewable i
|
|
|
308
358
|
|
|
309
359
|
Native JS workflows persist to:
|
|
310
360
|
|
|
311
|
-
- `workflows/<name>/state/current.json` — status, memory, pending interaction
|
|
361
|
+
- `workflows/<name>/state/current.json` — status, memory (includes fileTree), pending interaction
|
|
312
362
|
- `workflows/<name>/state/history.jsonl` — event log (newest entries first, includes agent retry/failure entries)
|
|
313
363
|
- `workflows/<name>/interactions/*.md` — human input files (when paused)
|
|
314
364
|
|
package/bin/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { pathToFileURL, fileURLToPath } from 'url';
|
|
|
7
7
|
import { WorkflowRuntime } from '../lib/index.js';
|
|
8
8
|
import { setup } from '../lib/setup.js';
|
|
9
9
|
import { generateSessionToken } from '../lib/remote/client.js';
|
|
10
|
+
import { readRemotePathFromConfig, writeRemotePathToConfig } from '../lib/config-utils.js';
|
|
10
11
|
|
|
11
12
|
import { startLocalServer } from '../vercel-server/local-server.js';
|
|
12
13
|
|
|
@@ -119,146 +120,6 @@ function resolveWorkflowEntry(workflowDir) {
|
|
|
119
120
|
return null;
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
function findConfigObjectRange(source) {
|
|
123
|
-
const match = source.match(/export\s+const\s+config\s*=/);
|
|
124
|
-
if (!match) return null;
|
|
125
|
-
const startSearch = match.index + match[0].length;
|
|
126
|
-
const braceStart = source.indexOf('{', startSearch);
|
|
127
|
-
if (braceStart === -1) return null;
|
|
128
|
-
|
|
129
|
-
let depth = 0;
|
|
130
|
-
let inString = null;
|
|
131
|
-
let inLineComment = false;
|
|
132
|
-
let inBlockComment = false;
|
|
133
|
-
let escape = false;
|
|
134
|
-
|
|
135
|
-
for (let i = braceStart; i < source.length; i += 1) {
|
|
136
|
-
const ch = source[i];
|
|
137
|
-
const next = source[i + 1];
|
|
138
|
-
|
|
139
|
-
if (inLineComment) {
|
|
140
|
-
if (ch === '\n') inLineComment = false;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (inBlockComment) {
|
|
145
|
-
if (ch === '*' && next === '/') {
|
|
146
|
-
inBlockComment = false;
|
|
147
|
-
i += 1;
|
|
148
|
-
}
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (inString) {
|
|
153
|
-
if (escape) {
|
|
154
|
-
escape = false;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (ch === '\\') {
|
|
158
|
-
escape = true;
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (ch === inString) {
|
|
162
|
-
inString = null;
|
|
163
|
-
}
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
168
|
-
inString = ch;
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (ch === '/' && next === '/') {
|
|
173
|
-
inLineComment = true;
|
|
174
|
-
i += 1;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (ch === '/' && next === '*') {
|
|
179
|
-
inBlockComment = true;
|
|
180
|
-
i += 1;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (ch === '{') {
|
|
185
|
-
depth += 1;
|
|
186
|
-
} else if (ch === '}') {
|
|
187
|
-
depth -= 1;
|
|
188
|
-
if (depth === 0) {
|
|
189
|
-
return { start: braceStart, end: i };
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function readRemotePathFromConfig(configFile) {
|
|
198
|
-
const source = fs.readFileSync(configFile, 'utf-8');
|
|
199
|
-
const range = findConfigObjectRange(source);
|
|
200
|
-
if (!range) return null;
|
|
201
|
-
const configSource = source.slice(range.start, range.end + 1);
|
|
202
|
-
const match = configSource.match(/\bremotePath\s*:\s*(['"`])([^'"`]+)\1/);
|
|
203
|
-
return match ? match[2] : null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function writeRemotePathToConfig(configFile, remotePath) {
|
|
207
|
-
const source = fs.readFileSync(configFile, 'utf-8');
|
|
208
|
-
const range = findConfigObjectRange(source);
|
|
209
|
-
const remoteLine = `remotePath: "${remotePath}"`;
|
|
210
|
-
|
|
211
|
-
if (!range) {
|
|
212
|
-
const hasConfigExport = /export\s+const\s+config\s*=/.test(source);
|
|
213
|
-
if (hasConfigExport) {
|
|
214
|
-
throw new Error('Config export is not an object literal; add remotePath manually.');
|
|
215
|
-
}
|
|
216
|
-
const trimmed = source.replace(/\s*$/, '');
|
|
217
|
-
const appended = `${trimmed}\n\nexport const config = {\n ${remoteLine}\n};\n`;
|
|
218
|
-
fs.writeFileSync(configFile, appended);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const configSource = source.slice(range.start, range.end + 1);
|
|
223
|
-
const remoteRegex = /\bremotePath\s*:\s*(['"`])([^'"`]*?)\1/;
|
|
224
|
-
let updatedConfigSource;
|
|
225
|
-
|
|
226
|
-
if (remoteRegex.test(configSource)) {
|
|
227
|
-
updatedConfigSource = configSource.replace(remoteRegex, remoteLine);
|
|
228
|
-
} else {
|
|
229
|
-
const inner = configSource.slice(1, -1);
|
|
230
|
-
const indentMatch = inner.match(/\n([ \t]+)\S/);
|
|
231
|
-
const indent = indentMatch ? indentMatch[1] : ' ';
|
|
232
|
-
const trimmedInner = inner.replace(/\s*$/, '');
|
|
233
|
-
const hasContent = trimmedInner.trim().length > 0;
|
|
234
|
-
let updatedInner = trimmedInner;
|
|
235
|
-
|
|
236
|
-
if (hasContent) {
|
|
237
|
-
for (let i = updatedInner.length - 1; i >= 0; i -= 1) {
|
|
238
|
-
const ch = updatedInner[i];
|
|
239
|
-
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') continue;
|
|
240
|
-
if (ch !== ',') {
|
|
241
|
-
updatedInner += ',';
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const needsNewline = updatedInner && !updatedInner.endsWith('\n');
|
|
248
|
-
const insert = `${indent}${remoteLine},\n`;
|
|
249
|
-
const newInner = hasContent
|
|
250
|
-
? `${updatedInner}${needsNewline ? '\n' : ''}${insert}`
|
|
251
|
-
: `\n${insert}`;
|
|
252
|
-
updatedConfigSource = `{${newInner}}`;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const updatedSource =
|
|
256
|
-
source.slice(0, range.start) +
|
|
257
|
-
updatedConfigSource +
|
|
258
|
-
source.slice(range.end + 1);
|
|
259
|
-
fs.writeFileSync(configFile, updatedSource);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
123
|
function ensureRemotePath(configFile, { forceNew = false } = {}) {
|
|
263
124
|
const existing = readRemotePathFromConfig(configFile);
|
|
264
125
|
if (existing && !forceNew) return existing;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: /lib/config-utils.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
export function findConfigObjectRange(source) {
|
|
8
|
+
const match = source.match(/export\s+const\s+config\s*=/);
|
|
9
|
+
if (!match) return null;
|
|
10
|
+
const startSearch = match.index + match[0].length;
|
|
11
|
+
const braceStart = source.indexOf('{', startSearch);
|
|
12
|
+
if (braceStart === -1) return null;
|
|
13
|
+
|
|
14
|
+
return findObjectLiteralRange(source, braceStart);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findObjectLiteralRange(source, braceStart) {
|
|
18
|
+
let depth = 0;
|
|
19
|
+
let inString = null;
|
|
20
|
+
let inLineComment = false;
|
|
21
|
+
let inBlockComment = false;
|
|
22
|
+
let escape = false;
|
|
23
|
+
|
|
24
|
+
for (let i = braceStart; i < source.length; i += 1) {
|
|
25
|
+
const ch = source[i];
|
|
26
|
+
const next = source[i + 1];
|
|
27
|
+
|
|
28
|
+
if (inLineComment) {
|
|
29
|
+
if (ch === '\n') inLineComment = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (inBlockComment) {
|
|
34
|
+
if (ch === '*' && next === '/') {
|
|
35
|
+
inBlockComment = false;
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (inString) {
|
|
42
|
+
if (escape) {
|
|
43
|
+
escape = false;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '\\') {
|
|
47
|
+
escape = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === inString) {
|
|
51
|
+
inString = null;
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (ch === '"' || ch === '\'' || ch === '`') {
|
|
57
|
+
inString = ch;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (ch === '/' && next === '/') {
|
|
62
|
+
inLineComment = true;
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (ch === '/' && next === '*') {
|
|
68
|
+
inBlockComment = true;
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (ch === '{') {
|
|
74
|
+
depth += 1;
|
|
75
|
+
} else if (ch === '}') {
|
|
76
|
+
depth -= 1;
|
|
77
|
+
if (depth === 0) {
|
|
78
|
+
return { start: braceStart, end: i };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function detectIndent(inner) {
|
|
87
|
+
const indentMatch = inner.match(/\n([ \t]+)\S/);
|
|
88
|
+
return indentMatch ? indentMatch[1] : ' ';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function escapeRegExp(value) {
|
|
92
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isValidIdentifier(value) {
|
|
96
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatObjectKey(value) {
|
|
100
|
+
if (isValidIdentifier(value)) return value;
|
|
101
|
+
return JSON.stringify(value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function insertPropertyIntoObjectSource(objectSource, propertySource, indent) {
|
|
105
|
+
const inner = objectSource.slice(1, -1);
|
|
106
|
+
const trimmedInner = inner.replace(/\s*$/, '');
|
|
107
|
+
const hasContent = trimmedInner.trim().length > 0;
|
|
108
|
+
let updatedInner = trimmedInner;
|
|
109
|
+
|
|
110
|
+
if (hasContent) {
|
|
111
|
+
for (let i = updatedInner.length - 1; i >= 0; i -= 1) {
|
|
112
|
+
const ch = updatedInner[i];
|
|
113
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') continue;
|
|
114
|
+
if (ch !== ',') {
|
|
115
|
+
updatedInner += ',';
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const needsNewline = updatedInner && !updatedInner.endsWith('\n');
|
|
122
|
+
const insert = `${indent}${propertySource},\n`;
|
|
123
|
+
const newInner = hasContent
|
|
124
|
+
? `${updatedInner}${needsNewline ? '\n' : ''}${insert}`
|
|
125
|
+
: `\n${insert}`;
|
|
126
|
+
|
|
127
|
+
return `{${newInner}}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function readRemotePathFromConfig(configFile) {
|
|
131
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
132
|
+
const range = findConfigObjectRange(source);
|
|
133
|
+
if (!range) return null;
|
|
134
|
+
const configSource = source.slice(range.start, range.end + 1);
|
|
135
|
+
const match = configSource.match(/\bremotePath\s*:\s*(['"`])([^'"`]+)\1/);
|
|
136
|
+
return match ? match[2] : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function writeRemotePathToConfig(configFile, remotePath) {
|
|
140
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
141
|
+
const range = findConfigObjectRange(source);
|
|
142
|
+
const remoteLine = `remotePath: ${JSON.stringify(remotePath)}`;
|
|
143
|
+
|
|
144
|
+
if (!range) {
|
|
145
|
+
const hasConfigExport = /export\s+const\s+config\s*=/.test(source);
|
|
146
|
+
if (hasConfigExport) {
|
|
147
|
+
throw new Error('Config export is not an object literal; add remotePath manually.');
|
|
148
|
+
}
|
|
149
|
+
const trimmed = source.replace(/\s*$/, '');
|
|
150
|
+
const appended = `${trimmed}\n\nexport const config = {\n ${remoteLine}\n};\n`;
|
|
151
|
+
fs.writeFileSync(configFile, appended);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const configSource = source.slice(range.start, range.end + 1);
|
|
156
|
+
const remoteRegex = /\bremotePath\s*:\s*(['"`])([^'"`]*?)\1/;
|
|
157
|
+
let updatedConfigSource;
|
|
158
|
+
|
|
159
|
+
if (remoteRegex.test(configSource)) {
|
|
160
|
+
updatedConfigSource = configSource.replace(remoteRegex, remoteLine);
|
|
161
|
+
} else {
|
|
162
|
+
const inner = configSource.slice(1, -1);
|
|
163
|
+
const indent = detectIndent(inner);
|
|
164
|
+
updatedConfigSource = insertPropertyIntoObjectSource(configSource, remoteLine, indent);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const updatedSource =
|
|
168
|
+
source.slice(0, range.start) +
|
|
169
|
+
updatedConfigSource +
|
|
170
|
+
source.slice(range.end + 1);
|
|
171
|
+
fs.writeFileSync(configFile, updatedSource);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function findModelsObjectRange(configSource) {
|
|
175
|
+
const regex = /\bmodels\s*:\s*{/g;
|
|
176
|
+
const match = regex.exec(configSource);
|
|
177
|
+
if (!match) return null;
|
|
178
|
+
const braceStart = configSource.indexOf('{', match.index + match[0].length - 1);
|
|
179
|
+
if (braceStart === -1) return null;
|
|
180
|
+
return findObjectLiteralRange(configSource, braceStart);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildModelKeyRegex(modelKey) {
|
|
184
|
+
const escaped = escapeRegExp(modelKey);
|
|
185
|
+
const quotedKey = `['"\\\`]${escaped}['"\\\`]`;
|
|
186
|
+
const unquotedKey = isValidIdentifier(modelKey) ? escaped : null;
|
|
187
|
+
const keyPattern = unquotedKey ? `(?:${unquotedKey}|${quotedKey})` : quotedKey;
|
|
188
|
+
return new RegExp(`(^|[,{\\s])(${keyPattern})\\s*:\\s*(['"\\\`])([^'"\\\\\`]*?)\\3`, 'm');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function readModelFromConfig(configFile, modelKey) {
|
|
192
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
193
|
+
const range = findConfigObjectRange(source);
|
|
194
|
+
if (!range) return null;
|
|
195
|
+
const configSource = source.slice(range.start, range.end + 1);
|
|
196
|
+
const modelsRange = findModelsObjectRange(configSource);
|
|
197
|
+
if (!modelsRange) return null;
|
|
198
|
+
const modelsSource = configSource.slice(modelsRange.start, modelsRange.end + 1);
|
|
199
|
+
const keyRegex = buildModelKeyRegex(modelKey);
|
|
200
|
+
const match = modelsSource.match(keyRegex);
|
|
201
|
+
return match ? match[4] : null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function writeModelToConfig(configFile, modelKey, modelValue) {
|
|
205
|
+
const source = fs.readFileSync(configFile, 'utf-8');
|
|
206
|
+
const range = findConfigObjectRange(source);
|
|
207
|
+
const formattedKey = formatObjectKey(modelKey);
|
|
208
|
+
const serializedValue = JSON.stringify(modelValue);
|
|
209
|
+
|
|
210
|
+
if (!range) {
|
|
211
|
+
const hasConfigExport = /export\s+const\s+config\s*=/.test(source);
|
|
212
|
+
if (hasConfigExport) {
|
|
213
|
+
throw new Error('Config export is not an object literal; add models mapping manually.');
|
|
214
|
+
}
|
|
215
|
+
const trimmed = source.replace(/\s*$/, '');
|
|
216
|
+
const appended = `${trimmed}\n\nexport const config = {\n models: {\n ${formattedKey}: ${serializedValue}\n }\n};\n`;
|
|
217
|
+
fs.writeFileSync(configFile, appended);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const configSource = source.slice(range.start, range.end + 1);
|
|
222
|
+
const modelsRange = findModelsObjectRange(configSource);
|
|
223
|
+
let updatedConfigSource = configSource;
|
|
224
|
+
|
|
225
|
+
if (!modelsRange) {
|
|
226
|
+
const inner = configSource.slice(1, -1);
|
|
227
|
+
const indent = detectIndent(inner);
|
|
228
|
+
const nestedIndent = `${indent} `;
|
|
229
|
+
const modelsBlock = `models: {\n${nestedIndent}${formattedKey}: ${serializedValue},\n${indent}}`;
|
|
230
|
+
updatedConfigSource = insertPropertyIntoObjectSource(configSource, modelsBlock, indent);
|
|
231
|
+
} else {
|
|
232
|
+
const modelsSource = configSource.slice(modelsRange.start, modelsRange.end + 1);
|
|
233
|
+
const keyRegex = buildModelKeyRegex(modelKey);
|
|
234
|
+
let updatedModelsSource;
|
|
235
|
+
|
|
236
|
+
if (keyRegex.test(modelsSource)) {
|
|
237
|
+
updatedModelsSource = modelsSource.replace(
|
|
238
|
+
keyRegex,
|
|
239
|
+
(_match, prefix, keyToken) => `${prefix}${keyToken}: ${serializedValue}`
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
const inner = modelsSource.slice(1, -1);
|
|
243
|
+
const indent = detectIndent(inner);
|
|
244
|
+
const modelLine = `${formattedKey}: ${serializedValue}`;
|
|
245
|
+
updatedModelsSource = insertPropertyIntoObjectSource(modelsSource, modelLine, indent);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
updatedConfigSource =
|
|
249
|
+
configSource.slice(0, modelsRange.start) +
|
|
250
|
+
updatedModelsSource +
|
|
251
|
+
configSource.slice(modelsRange.end + 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const updatedSource =
|
|
255
|
+
source.slice(0, range.start) +
|
|
256
|
+
updatedConfigSource +
|
|
257
|
+
source.slice(range.end + 1);
|
|
258
|
+
fs.writeFileSync(configFile, updatedSource);
|
|
259
|
+
}
|