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 CHANGED
@@ -6,7 +6,10 @@ export interface ScaffoldOptions {
6
6
  templateDir?: string;
7
7
  }
8
8
  export declare function scaffold(options: ScaffoldOptions): string;
9
- export type UpgradeFileStatus = 'updated' | 'unchanged' | 'conflict' | 'skipped';
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
- execFileSync('npm', ['install'], { cwd: projectDir, stdio: 'pipe' });
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
- printUpgradeSummary(upgradeProject({ from }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "description": "Scaffold a new loopengine agent project — a runnable HTTP server with one example agent, in your own repo.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -6,3 +6,5 @@ dist
6
6
  .env
7
7
  *.log
8
8
  .DS_Store
9
+ # Backups the console Upgrade button keeps of the files it changed.
10
+ .upgrade-backups
@@ -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
- await execFileAsync('npm', ['install', ...deps], { cwd: process.cwd(), timeout: NPM_INSTALL_DEPS_TIMEOUT_MS })
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 }),
@@ -9,12 +9,12 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.75",
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
  }