create-loopengine 0.1.15 → 0.1.17

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 CHANGED
@@ -115,10 +115,17 @@ Each of the five template-owned files gets one of:
115
115
  as any git merge conflict — resolve them by hand, then `npx tsc
116
116
  --noEmit` to confirm it builds.
117
117
 
118
- `upgrade` never touches `agents/`, `README.md`, `.env.example`, or
119
- `package.json`'s own dependency versions — run `npm install
120
- loopengine@latest` (and `actauth`/`skillgarden` if you use them directly)
121
- separately to pick those up.
118
+ `upgrade` also bumps whichever of `loopengine`/`actauth`/`skillgarden`
119
+ are already in your `package.json` to their latest published version
120
+ (never adds one you don't already depend on) and runs `npm install` for
121
+ you — a merged file that now calls a newer export is a silent, hard-to-
122
+ notice failure if the installed package hasn't actually caught up, so
123
+ this isn't left as a manual step. If the install itself fails (offline,
124
+ a registry hiccup), the version bump is still written to `package.json`;
125
+ just run `npm install` yourself to finish.
126
+
127
+ `upgrade` never touches `agents/`, `README.md`, `.env.example`, or any
128
+ dependency *other* than those three.
122
129
 
123
130
  ## Running in production
124
131
 
package/dist/cli.d.ts CHANGED
@@ -11,10 +11,29 @@ export interface UpgradeFileResult {
11
11
  file: string;
12
12
  status: UpgradeFileStatus;
13
13
  }
14
+ export interface UpgradeDependencyResult {
15
+ name: string;
16
+ from: string;
17
+ to: string;
18
+ }
14
19
  export interface UpgradeResult {
15
20
  fromVersion: string;
16
21
  toVersion: string;
17
22
  files: UpgradeFileResult[];
23
+ /** Which of loopengine/actauth/skillgarden actually had their
24
+ * package.json range bumped — only ones already present as a
25
+ * dependency, never newly added (see bumpDependencies's own doc
26
+ * comment). Empty if the project has no package.json, or every present
27
+ * one was already at the latest range. */
28
+ dependencies: UpgradeDependencyResult[];
29
+ /** Only attempted when `dependencies` is non-empty — running `npm
30
+ * install` for a project with nothing to bump would be pointless. */
31
+ npmInstall?: {
32
+ ok: true;
33
+ } | {
34
+ ok: false;
35
+ error: string;
36
+ };
18
37
  }
19
38
  export interface UpgradeOptions {
20
39
  /** Defaults to process.cwd(). */
@@ -31,6 +50,13 @@ export interface UpgradeOptions {
31
50
  * (a real `npm pack`). Test-only seam to avoid hitting the real
32
51
  * registry for a fixture "old" template. */
33
52
  resolveBaseTemplateDir?: (version: string) => string;
53
+ /** Resolves a package's current latest version — defaults to a real
54
+ * `npm view <pkg> version`. Test-only seam to avoid hitting the real
55
+ * registry. */
56
+ resolveLatestVersion?: (pkg: string) => string;
57
+ /** Runs `npm install` in projectDir — defaults to a real one. Test-only
58
+ * seam to avoid actually installing anything during tests. */
59
+ runNpmInstall?: (projectDir: string) => void;
34
60
  }
35
61
  export declare function upgradeProject(options?: UpgradeOptions): UpgradeResult;
36
62
  export declare function main(argv?: string[]): void;
package/dist/cli.js CHANGED
@@ -163,8 +163,76 @@ export function upgradeProject(options = {}) {
163
163
  const resolveBase = options.resolveBaseTemplateDir ?? fetchPublishedTemplateDir;
164
164
  const baseTemplateDir = resolveBase(fromVersion);
165
165
  const files = UPGRADE_CANDIDATE_FILES.map((candidate) => upgradeFile(candidate, baseTemplateDir, currentTemplateDir, projectDir));
166
+ const resolveLatestVersion = options.resolveLatestVersion ?? resolveLatestVersionFromNpm;
167
+ const dependencies = bumpDependencies(projectDir, resolveLatestVersion);
168
+ let npmInstall;
169
+ if (dependencies.length > 0) {
170
+ const install = options.runNpmInstall ?? runRealNpmInstall;
171
+ try {
172
+ install(projectDir);
173
+ npmInstall = { ok: true };
174
+ }
175
+ catch (err) {
176
+ // Non-fatal — the file merge and the package.json bump already
177
+ // succeeded and are on disk either way; a failed install (offline,
178
+ // a registry hiccup) just means the operator runs `npm install`
179
+ // themselves, the same single command this was trying to save them
180
+ // from typing, not a reason to unwind everything already done.
181
+ npmInstall = { ok: false, error: err instanceof Error ? err.message : String(err) };
182
+ }
183
+ }
166
184
  writeFileSync(provenancePath, JSON.stringify({ version: toVersion }, null, 2) + '\n');
167
- return { fromVersion, toVersion, files };
185
+ return { fromVersion, toVersion, files, dependencies, npmInstall };
186
+ }
187
+ function resolveLatestVersionFromNpm(pkg) {
188
+ return execFileSync('npm', ['view', pkg, 'version'], { stdio: 'pipe' }).toString().trim();
189
+ }
190
+ function runRealNpmInstall(projectDir) {
191
+ execFileSync('npm', ['install'], { cwd: projectDir, stdio: 'pipe' });
192
+ }
193
+ // Only loopengine/actauth/skillgarden — the three packages a scaffolded
194
+ // project's own adapters/http.ts can import directly (see this repo's own
195
+ // README on why: template code that imports a package must declare it as
196
+ // its own direct dependency, not rely on loopengine's transitive one) —
197
+ // and only ones already present in the project's own package.json.
198
+ // Deliberately never *adds* a dependency a project doesn't already have:
199
+ // this command upgrades what's there, it doesn't decide what a project
200
+ // should depend on.
201
+ const UPGRADABLE_DEPENDENCIES = ['loopengine', 'actauth', 'skillgarden'];
202
+ /** Bumps package.json's dependency ranges for whichever of
203
+ * loopengine/actauth/skillgarden are already present, to `^<latest>` —
204
+ * run automatically as part of `upgrade` (not left as a manual follow-up
205
+ * step) because a project whose merged template code now calls a newer
206
+ * export, but whose installed package is still old, fails silently: no
207
+ * conflict marker, no build error, just a feature that's quietly not
208
+ * there yet (confirmed live — this exact gap is what prompted adding this
209
+ * function in the first place, see this repo's own git history). Returns
210
+ * [] untouched if the project has no package.json at all, same
211
+ * "can't merge what isn't there" reasoning upgradeFile's own missing-file
212
+ * check uses. */
213
+ function bumpDependencies(projectDir, resolveLatestVersion) {
214
+ const pkgPath = join(projectDir, 'package.json');
215
+ if (!existsSync(pkgPath))
216
+ return [];
217
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
218
+ const deps = pkg.dependencies;
219
+ if (!deps)
220
+ return [];
221
+ const changes = [];
222
+ for (const name of UPGRADABLE_DEPENDENCIES) {
223
+ const currentRange = deps[name];
224
+ if (currentRange === undefined)
225
+ continue;
226
+ const latest = resolveLatestVersion(name);
227
+ const newRange = `^${latest}`;
228
+ if (currentRange === newRange)
229
+ continue;
230
+ deps[name] = newRange;
231
+ changes.push({ name, from: currentRange, to: newRange });
232
+ }
233
+ if (changes.length > 0)
234
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
235
+ return changes;
168
236
  }
169
237
  function parseUpgradeArgs(argv) {
170
238
  let from;
@@ -187,7 +255,18 @@ function printUpgradeSummary(result) {
187
255
  console.log(`${conflicted.length} file(s) have <<<<<<< conflict markers to resolve by hand before this will build.`);
188
256
  }
189
257
  else {
190
- console.log('No conflicts. Run `npm install loopengine@latest` to pick up any library-side changes too.');
258
+ console.log('No conflicts.');
259
+ }
260
+ if (result.dependencies.length > 0) {
261
+ console.log();
262
+ for (const dep of result.dependencies)
263
+ console.log(` ${dep.name}: ${dep.from} -> ${dep.to}`);
264
+ if (result.npmInstall?.ok) {
265
+ console.log('npm install completed.');
266
+ }
267
+ else if (result.npmInstall) {
268
+ console.log(`npm install failed (${result.npmInstall.error}) — run it yourself to finish picking up the new versions.`);
269
+ }
191
270
  }
192
271
  }
193
272
  function parseArgs(argv) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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",
@@ -524,6 +524,13 @@ async function handleHttpToolPost(req: IncomingMessage, res: ServerResponse, age
524
524
  return
525
525
  }
526
526
 
527
+ // Resolved *before* createHttpTool writes anything to disk. Reading it
528
+ // after would risk a cold import of the freshly-written tools/index.ts
529
+ // (folder-form agents don't cache config.tools) — which already contains
530
+ // the new tool by then, so the ...currentTools, tool below would append
531
+ // it a second time.
532
+ const currentTools = entry.config.tools ?? (await loadDefaultTools(entry.config))
533
+
527
534
  let tool: ToolDefinition
528
535
  try {
529
536
  const result = await createHttpTool(agentName, parsed.value)
@@ -534,7 +541,6 @@ async function handleHttpToolPost(req: IncomingMessage, res: ServerResponse, age
534
541
  return
535
542
  }
536
543
 
537
- const currentTools = entry.config.tools ?? (await loadDefaultTools(entry.config))
538
544
  updateAgent(agentName, { config: { tools: [...currentTools, tool] } })
539
545
 
540
546
  if (parsed.decision) {