create-loopengine 0.1.60 → 0.1.62
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/cli.d.ts +9 -1
- package/dist/cli.js +38 -7
- package/package.json +1 -1
- package/template/_gitignore +2 -0
- package/template/adapters/http.ts +29 -1
- package/template/package.json +5 -5
package/dist/cli.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export interface ScaffoldOptions {
|
|
|
6
6
|
templateDir?: string;
|
|
7
7
|
}
|
|
8
8
|
export declare function scaffold(options: ScaffoldOptions): string;
|
|
9
|
-
|
|
9
|
+
/** `kept`: the project edited a file the template also changed, and
|
|
10
|
+
* `keepEdited` was set, so it was left exactly as it was (no merge, no
|
|
11
|
+
* conflict markers). */
|
|
12
|
+
export type UpgradeFileStatus = 'updated' | 'unchanged' | 'conflict' | 'skipped' | 'kept';
|
|
10
13
|
export interface UpgradeFileResult {
|
|
11
14
|
file: string;
|
|
12
15
|
status: UpgradeFileStatus;
|
|
@@ -57,6 +60,11 @@ export interface UpgradeOptions {
|
|
|
57
60
|
/** Runs `npm install` in projectDir — defaults to a real one. Test-only
|
|
58
61
|
* seam to avoid actually installing anything during tests. */
|
|
59
62
|
runNpmInstall?: (projectDir: string) => void;
|
|
63
|
+
/** Leave a file the project edited untouched (status `kept`) instead of
|
|
64
|
+
* three-way merging it, so nothing ever gets conflict markers. For
|
|
65
|
+
* unattended upgrades (the console's Upgrade button): a half-merged
|
|
66
|
+
* adapters/http.ts would stop the server from starting at all. */
|
|
67
|
+
keepEdited?: boolean;
|
|
60
68
|
}
|
|
61
69
|
export declare function upgradeProject(options?: UpgradeOptions): UpgradeResult;
|
|
62
70
|
export declare function main(argv?: string[]): void;
|
package/dist/cli.js
CHANGED
|
@@ -116,7 +116,7 @@ function threeWayMerge(minePath, basePath, theirsPath) {
|
|
|
116
116
|
}
|
|
117
117
|
return { merged: readFileSync(minePath, 'utf8'), conflicted };
|
|
118
118
|
}
|
|
119
|
-
function upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir) {
|
|
119
|
+
function upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir, keepEdited = false) {
|
|
120
120
|
const projectFilePath = join(projectDir, candidate.projectPath);
|
|
121
121
|
const baseFilePath = join(baseTemplateDir, candidate.templatePath);
|
|
122
122
|
const theirsFilePath = join(currentTemplateDir, candidate.templatePath);
|
|
@@ -132,6 +132,17 @@ function upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir)
|
|
|
132
132
|
return { file: candidate.projectPath, status: 'unchanged' };
|
|
133
133
|
}
|
|
134
134
|
const mine = readFileSync(projectFilePath, 'utf8');
|
|
135
|
+
// The common cases need no merge (and so no git): a file the project
|
|
136
|
+
// never edited takes the new template as is, and one that already
|
|
137
|
+
// matches it has nothing to do.
|
|
138
|
+
if (mine === theirs)
|
|
139
|
+
return { file: candidate.projectPath, status: 'unchanged' };
|
|
140
|
+
if (mine === base) {
|
|
141
|
+
writeFileSync(projectFilePath, theirs);
|
|
142
|
+
return { file: candidate.projectPath, status: 'updated' };
|
|
143
|
+
}
|
|
144
|
+
if (keepEdited)
|
|
145
|
+
return { file: candidate.projectPath, status: 'kept' };
|
|
135
146
|
const scratchDir = mkdtempSync(join(tmpdir(), 'create-loopengine-merge-'));
|
|
136
147
|
const mineTmp = join(scratchDir, 'mine');
|
|
137
148
|
const baseTmp = join(scratchDir, 'base');
|
|
@@ -162,7 +173,7 @@ export function upgradeProject(options = {}) {
|
|
|
162
173
|
}
|
|
163
174
|
const resolveBase = options.resolveBaseTemplateDir ?? fetchPublishedTemplateDir;
|
|
164
175
|
const baseTemplateDir = resolveBase(fromVersion);
|
|
165
|
-
const files = UPGRADE_CANDIDATE_FILES.map((candidate) => upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir));
|
|
176
|
+
const files = UPGRADE_CANDIDATE_FILES.map((candidate) => upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir, options.keepEdited));
|
|
166
177
|
const resolveLatestVersion = options.resolveLatestVersion ?? resolveLatestVersionFromNpm;
|
|
167
178
|
const dependencies = bumpDependencies(projectDir, resolveLatestVersion);
|
|
168
179
|
let npmInstall;
|
|
@@ -188,7 +199,9 @@ function resolveLatestVersionFromNpm(pkg) {
|
|
|
188
199
|
return execFileSync('npm', ['view', pkg, 'version'], { stdio: 'pipe' }).toString().trim();
|
|
189
200
|
}
|
|
190
201
|
function runRealNpmInstall(projectDir) {
|
|
191
|
-
|
|
202
|
+
// --include=dev: with NODE_ENV=production npm would drop devDependencies,
|
|
203
|
+
// and an older project still lists tsx, which runs it, as one.
|
|
204
|
+
execFileSync('npm', ['install', '--include=dev'], { cwd: projectDir, stdio: 'pipe' });
|
|
192
205
|
}
|
|
193
206
|
// Only loopengine/actauth/skillgarden — the three packages a scaffolded
|
|
194
207
|
// project's own adapters/http.ts can import directly (see this repo's own
|
|
@@ -236,17 +249,23 @@ function bumpDependencies(projectDir, resolveLatestVersion) {
|
|
|
236
249
|
}
|
|
237
250
|
function parseUpgradeArgs(argv) {
|
|
238
251
|
let from;
|
|
252
|
+
let json = false;
|
|
253
|
+
let keepEdited = false;
|
|
239
254
|
for (let i = 0; i < argv.length; i++) {
|
|
240
255
|
if (argv[i] === '--from')
|
|
241
256
|
from = argv[++i];
|
|
257
|
+
else if (argv[i] === '--json')
|
|
258
|
+
json = true;
|
|
259
|
+
else if (argv[i] === '--keep-edited')
|
|
260
|
+
keepEdited = true;
|
|
242
261
|
}
|
|
243
|
-
return { from };
|
|
262
|
+
return { from, json, keepEdited };
|
|
244
263
|
}
|
|
245
264
|
function printUpgradeSummary(result) {
|
|
246
265
|
console.log(`Upgrading create-loopengine@${result.fromVersion} -> @${result.toVersion}`);
|
|
247
266
|
console.log();
|
|
248
267
|
for (const f of result.files) {
|
|
249
|
-
const label = f.status === 'updated' ? 'updated ' : f.status === 'conflict' ? 'conflict ' : f.status === 'unchanged' ? 'unchanged' : 'skipped ';
|
|
268
|
+
const label = f.status === 'updated' ? 'updated ' : f.status === 'conflict' ? 'conflict ' : f.status === 'unchanged' ? 'unchanged' : f.status === 'kept' ? 'kept ' : 'skipped ';
|
|
250
269
|
console.log(` ${label} ${f.file}`);
|
|
251
270
|
}
|
|
252
271
|
console.log();
|
|
@@ -275,8 +294,20 @@ function parseArgs(argv) {
|
|
|
275
294
|
export function main(argv = process.argv.slice(2)) {
|
|
276
295
|
try {
|
|
277
296
|
if (argv[0] === 'upgrade') {
|
|
278
|
-
const { from } = parseUpgradeArgs(argv.slice(1));
|
|
279
|
-
|
|
297
|
+
const { from, json, keepEdited } = parseUpgradeArgs(argv.slice(1));
|
|
298
|
+
if (json) {
|
|
299
|
+
// One JSON object on stdout, for a program driving the upgrade
|
|
300
|
+
// (loopengine's console); errors become { error } with exit 1.
|
|
301
|
+
try {
|
|
302
|
+
console.log(JSON.stringify(upgradeProject({ from, keepEdited })));
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
306
|
+
process.exitCode = 1;
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
printUpgradeSummary(upgradeProject({ from, keepEdited }));
|
|
280
311
|
return;
|
|
281
312
|
}
|
|
282
313
|
const { name } = parseArgs(argv);
|
package/package.json
CHANGED
package/template/_gitignore
CHANGED
|
@@ -154,6 +154,9 @@ import {
|
|
|
154
154
|
fetchLatestAbilityVersion,
|
|
155
155
|
describeModelProviders,
|
|
156
156
|
loopengineVersion,
|
|
157
|
+
describeUpgrade,
|
|
158
|
+
runUpgrade,
|
|
159
|
+
UpgradeError,
|
|
157
160
|
setModelProviderKey,
|
|
158
161
|
clearModelProviderKey,
|
|
159
162
|
ModelKeyError,
|
|
@@ -1565,7 +1568,9 @@ async function handleAbilityInstallDepsPost(req: IncomingMessage, res: ServerRes
|
|
|
1565
1568
|
return
|
|
1566
1569
|
}
|
|
1567
1570
|
try {
|
|
1568
|
-
|
|
1571
|
+
// --include=dev: under NODE_ENV=production npm would otherwise drop
|
|
1572
|
+
// devDependencies, including tsx, which runs this project.
|
|
1573
|
+
await execFileAsync('npm', ['install', '--include=dev', ...deps], { cwd: process.cwd(), timeout: NPM_INSTALL_DEPS_TIMEOUT_MS })
|
|
1569
1574
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ installed: deps }))
|
|
1570
1575
|
} catch (err) {
|
|
1571
1576
|
const stderr = err && typeof err === 'object' && 'stderr' in err ? String((err as { stderr: unknown }).stderr) : undefined
|
|
@@ -3008,6 +3013,29 @@ const server = createServer(async (req, res) => {
|
|
|
3008
3013
|
)
|
|
3009
3014
|
return
|
|
3010
3015
|
}
|
|
3016
|
+
// The Settings section's Upgrade button (web/upgrade-admin.ts).
|
|
3017
|
+
if (req.method === 'GET' && pathname === '/config/upgrade') {
|
|
3018
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(
|
|
3019
|
+
JSON.stringify({ ...(await describeUpgrade()), canUpgrade: secretsProtected, restartsItself: exitOnCodeChange }),
|
|
3020
|
+
)
|
|
3021
|
+
return
|
|
3022
|
+
}
|
|
3023
|
+
if (req.method === 'POST' && pathname === '/config/upgrade') {
|
|
3024
|
+
if (!secretsProtected) {
|
|
3025
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'Refusing to upgrade without LOOPENGINE_ADMIN_AUTH configured.' }))
|
|
3026
|
+
return
|
|
3027
|
+
}
|
|
3028
|
+
try {
|
|
3029
|
+
const outcome = await runUpgrade({ busy: () => sessionTurns.size + scheduler.running > 0 })
|
|
3030
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, outcome, restarting: exitOnCodeChange }))
|
|
3031
|
+
} catch (err) {
|
|
3032
|
+
const status = err instanceof UpgradeError ? 409 : 500
|
|
3033
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
3034
|
+
return
|
|
3035
|
+
}
|
|
3036
|
+
exitIfCodeChanged(res)
|
|
3037
|
+
return
|
|
3038
|
+
}
|
|
3011
3039
|
if (req.method === 'GET' && pathname === '/config/models') {
|
|
3012
3040
|
res.writeHead(200, { 'content-type': 'application/json' }).end(
|
|
3013
3041
|
JSON.stringify({ ...describeModelProviders(listAgents(), (name) => getEntry(name)?.config), canSave: secretsProtected }),
|
package/template/package.json
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"cli": "tsx adapters/cli.ts"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"loopengine": "^0.1.
|
|
13
|
-
"actauth": "^0.0.12"
|
|
14
|
-
},
|
|
15
|
-
"devDependencies": {
|
|
16
|
-
"@types/node": "^22.10.0",
|
|
12
|
+
"loopengine": "^0.1.77",
|
|
13
|
+
"actauth": "^0.0.12",
|
|
17
14
|
"tsx": "^4.19.0",
|
|
18
15
|
"typescript": "^5.7.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.10.0"
|
|
19
19
|
}
|
|
20
20
|
}
|