create-loopengine 0.1.60 → 0.1.61
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 +35 -6
- package/package.json +1 -1
- package/template/_gitignore +2 -0
- package/template/adapters/http.ts +26 -0
- package/template/package.json +1 -1
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;
|
|
@@ -236,17 +247,23 @@ function bumpDependencies(projectDir, resolveLatestVersion) {
|
|
|
236
247
|
}
|
|
237
248
|
function parseUpgradeArgs(argv) {
|
|
238
249
|
let from;
|
|
250
|
+
let json = false;
|
|
251
|
+
let keepEdited = false;
|
|
239
252
|
for (let i = 0; i < argv.length; i++) {
|
|
240
253
|
if (argv[i] === '--from')
|
|
241
254
|
from = argv[++i];
|
|
255
|
+
else if (argv[i] === '--json')
|
|
256
|
+
json = true;
|
|
257
|
+
else if (argv[i] === '--keep-edited')
|
|
258
|
+
keepEdited = true;
|
|
242
259
|
}
|
|
243
|
-
return { from };
|
|
260
|
+
return { from, json, keepEdited };
|
|
244
261
|
}
|
|
245
262
|
function printUpgradeSummary(result) {
|
|
246
263
|
console.log(`Upgrading create-loopengine@${result.fromVersion} -> @${result.toVersion}`);
|
|
247
264
|
console.log();
|
|
248
265
|
for (const f of result.files) {
|
|
249
|
-
const label = f.status === 'updated' ? 'updated ' : f.status === 'conflict' ? 'conflict ' : f.status === 'unchanged' ? 'unchanged' : 'skipped ';
|
|
266
|
+
const label = f.status === 'updated' ? 'updated ' : f.status === 'conflict' ? 'conflict ' : f.status === 'unchanged' ? 'unchanged' : f.status === 'kept' ? 'kept ' : 'skipped ';
|
|
250
267
|
console.log(` ${label} ${f.file}`);
|
|
251
268
|
}
|
|
252
269
|
console.log();
|
|
@@ -275,8 +292,20 @@ function parseArgs(argv) {
|
|
|
275
292
|
export function main(argv = process.argv.slice(2)) {
|
|
276
293
|
try {
|
|
277
294
|
if (argv[0] === 'upgrade') {
|
|
278
|
-
const { from } = parseUpgradeArgs(argv.slice(1));
|
|
279
|
-
|
|
295
|
+
const { from, json, keepEdited } = parseUpgradeArgs(argv.slice(1));
|
|
296
|
+
if (json) {
|
|
297
|
+
// One JSON object on stdout, for a program driving the upgrade
|
|
298
|
+
// (loopengine's console); errors become { error } with exit 1.
|
|
299
|
+
try {
|
|
300
|
+
console.log(JSON.stringify(upgradeProject({ from, keepEdited })));
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
304
|
+
process.exitCode = 1;
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
printUpgradeSummary(upgradeProject({ from, keepEdited }));
|
|
280
309
|
return;
|
|
281
310
|
}
|
|
282
311
|
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,
|
|
@@ -3008,6 +3011,29 @@ const server = createServer(async (req, res) => {
|
|
|
3008
3011
|
)
|
|
3009
3012
|
return
|
|
3010
3013
|
}
|
|
3014
|
+
// The Settings section's Upgrade button (web/upgrade-admin.ts).
|
|
3015
|
+
if (req.method === 'GET' && pathname === '/config/upgrade') {
|
|
3016
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(
|
|
3017
|
+
JSON.stringify({ ...(await describeUpgrade()), canUpgrade: secretsProtected, restartsItself: exitOnCodeChange }),
|
|
3018
|
+
)
|
|
3019
|
+
return
|
|
3020
|
+
}
|
|
3021
|
+
if (req.method === 'POST' && pathname === '/config/upgrade') {
|
|
3022
|
+
if (!secretsProtected) {
|
|
3023
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(JSON.stringify({ error: 'Refusing to upgrade without LOOPENGINE_ADMIN_AUTH configured.' }))
|
|
3024
|
+
return
|
|
3025
|
+
}
|
|
3026
|
+
try {
|
|
3027
|
+
const outcome = await runUpgrade({ busy: () => sessionTurns.size + scheduler.running > 0 })
|
|
3028
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, outcome, restarting: exitOnCodeChange }))
|
|
3029
|
+
} catch (err) {
|
|
3030
|
+
const status = err instanceof UpgradeError ? 409 : 500
|
|
3031
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
3032
|
+
return
|
|
3033
|
+
}
|
|
3034
|
+
exitIfCodeChanged(res)
|
|
3035
|
+
return
|
|
3036
|
+
}
|
|
3011
3037
|
if (req.method === 'GET' && pathname === '/config/models') {
|
|
3012
3038
|
res.writeHead(200, { 'content-type': 'application/json' }).end(
|
|
3013
3039
|
JSON.stringify({ ...describeModelProviders(listAgents(), (name) => getEntry(name)?.config), canSave: secretsProtected }),
|