@caiqueoak/flow 0.3.2 → 0.3.3

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/cli.mjs +64 -27
package/README.md CHANGED
@@ -48,9 +48,9 @@ Updates are explicit:
48
48
  flow update
49
49
  ```
50
50
 
51
- `flow update` detects how the active Flow CLI is installed. If the project contains `@caiqueoak/flow`, it updates that project dependency; if the CLI is globally installed, it updates the global package instead. It then refreshes `/flow` for every coding agent configured in `.flow/config.yaml` without modifying canonical project state.
51
+ `flow update` updates the installation that provides the active CLI: the global package for `flow update`, or the project dependency for `npx flow update`. It uses npm's update operation and refreshes `/flow` for every coding agent configured in `.flow/config.yaml` without modifying canonical project state. If the project lockfile and the package on disk disagree, Flow safely reinstalls only its own package before updating.
52
52
 
53
- On Windows, npm is invoked through the command shell so `npm.cmd` can be executed correctly. When Flow is installed as a project dependency, `npx flow update` is equivalent.
53
+ On Windows, npm is invoked through `cmd.exe` without Node's `shell: true` option, so `npm.cmd` executes without the `DEP0190` warning. When Flow is installed as a project dependency, `npx flow update` is equivalent.
54
54
 
55
55
  There is no background update check or automatic update mechanism.
56
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caiqueoak/flow",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Readability-first, agent-agnostic software development workflow for coding agents.",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/cli.mjs CHANGED
@@ -54,9 +54,11 @@ function question(rl, message) {
54
54
  }
55
55
 
56
56
  function runNpm(npmArgs, options = {}) {
57
+ if (process.platform === 'win32') {
58
+ return execFileSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', npmCommand(), ...npmArgs], options);
59
+ }
57
60
  return execFileSync(npmCommand(), npmArgs, {
58
- ...options,
59
- shell: process.platform === 'win32'
61
+ ...options
60
62
  });
61
63
  }
62
64
 
@@ -190,17 +192,6 @@ async function initProject() {
190
192
  else { info(); info('Flow is ready. Open a configured coding agent and invoke /flow.'); }
191
193
  }
192
194
 
193
- function dependencySection(root) {
194
- const file = path.join(root, 'package.json');
195
- if (!fs.existsSync(file)) return '--save-dev';
196
- try {
197
- const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
198
- if (pkg.dependencies?.[PACKAGE_NAME]) return '--save';
199
- if (pkg.optionalDependencies?.[PACKAGE_NAME]) return '--save-optional';
200
- } catch { /* use default */ }
201
- return '--save-dev';
202
- }
203
-
204
195
  function containingNodeModules(packageRoot) {
205
196
  let current = path.resolve(packageRoot);
206
197
  while (true) {
@@ -211,44 +202,88 @@ function containingNodeModules(packageRoot) {
211
202
  }
212
203
  }
213
204
 
214
- function updatePlan(root) {
215
- const projectPackageRoot = packagePath(root);
216
- if (fs.existsSync(path.join(projectPackageRoot, 'package.json'))) {
217
- return {
218
- npmArgs: ['install', dependencySection(root), `${PACKAGE_NAME}@latest`],
219
- cwd: root,
220
- packageRoot: projectPackageRoot,
221
- mode: 'project'
222
- };
223
- }
205
+ function installedPackageVersion(packageRoot) {
206
+ const manifest = path.join(packageRoot, 'package.json');
207
+ if (!fs.existsSync(manifest)) return null;
208
+ return JSON.parse(fs.readFileSync(manifest, 'utf8')).version || null;
209
+ }
224
210
 
211
+ function lockedPackageVersion(root) {
212
+ const lockfile = path.join(root, 'package-lock.json');
213
+ if (!fs.existsSync(lockfile)) return null;
214
+ try {
215
+ return JSON.parse(fs.readFileSync(lockfile, 'utf8')).packages?.[`node_modules/${PACKAGE_NAME}`]?.version || null;
216
+ } catch { return null; }
217
+ }
218
+
219
+ function globalUpdatePlan(root) {
225
220
  try {
226
221
  const globalNodeModules = path.resolve(runNpm(['root', '--global'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim());
227
222
  const globalPackageRoot = path.join(globalNodeModules, '@caiqueoak', 'flow');
228
223
  if (path.resolve(ROOT).startsWith(`${globalNodeModules}${path.sep}`) && fs.existsSync(path.join(globalPackageRoot, 'package.json'))) {
229
224
  return {
230
- npmArgs: ['install', '--global', `${PACKAGE_NAME}@latest`],
225
+ npmArgs: ['update', '--global', PACKAGE_NAME],
231
226
  cwd: root,
232
227
  packageRoot: globalPackageRoot,
233
228
  mode: 'global'
234
229
  };
235
230
  }
236
231
  } catch { /* fall through to local installation detection */ }
232
+ return null;
233
+ }
234
+
235
+ function updatePlan(root) {
236
+ const global = globalUpdatePlan(root);
237
+ if (global) return global;
237
238
 
238
239
  const nodeModules = containingNodeModules(ROOT);
239
240
  if (nodeModules) {
240
241
  const installRoot = path.dirname(nodeModules);
241
242
  return {
242
- npmArgs: ['install', dependencySection(installRoot), `${PACKAGE_NAME}@latest`],
243
+ npmArgs: ['update', PACKAGE_NAME],
243
244
  cwd: installRoot,
244
245
  packageRoot: ROOT,
245
246
  mode: 'local'
246
247
  };
247
248
  }
248
249
 
250
+ const projectPackageRoot = packagePath(root);
251
+ if (fs.existsSync(path.join(projectPackageRoot, 'package.json'))) {
252
+ return {
253
+ npmArgs: ['update', PACKAGE_NAME],
254
+ cwd: root,
255
+ packageRoot: projectPackageRoot,
256
+ mode: 'project'
257
+ };
258
+ }
259
+
249
260
  fail('cannot determine how this Flow CLI was installed. Reinstall @caiqueoak/flow with npm, then run flow update again.');
250
261
  }
251
262
 
263
+ function repairDivergentProjectInstall(plan) {
264
+ if (plan.mode === 'global') return;
265
+ const expected = lockedPackageVersion(plan.cwd);
266
+ const actual = installedPackageVersion(plan.packageRoot);
267
+ if (!expected || !actual || expected === actual) return;
268
+
269
+ const backup = path.join(plan.cwd, `.flow-update-backup-${process.pid}-${Date.now()}`);
270
+ info(`Repairing divergent ${PACKAGE_NAME} installation (${actual} on disk, ${expected} in package-lock.json)...`);
271
+ fs.renameSync(plan.packageRoot, backup);
272
+ try {
273
+ runNpm(['install'], { cwd: plan.cwd, stdio: 'inherit' });
274
+ if (installedPackageVersion(plan.packageRoot) !== expected) {
275
+ throw new Error(`npm install did not restore ${PACKAGE_NAME}@${expected}.`);
276
+ }
277
+ fs.rmSync(backup, { recursive: true, force: true });
278
+ } catch (error) {
279
+ try {
280
+ fs.rmSync(plan.packageRoot, { recursive: true, force: true });
281
+ fs.renameSync(backup, plan.packageRoot);
282
+ } catch { /* preserve the original npm error below */ }
283
+ throw error;
284
+ }
285
+ }
286
+
252
287
  function update() {
253
288
  const root = projectRoot();
254
289
  const config = readConfig(root);
@@ -257,8 +292,10 @@ function update() {
257
292
 
258
293
  const plan = updatePlan(root);
259
294
  info(`Updating ${PACKAGE_NAME} (${plan.mode} installation)...`);
260
- try { runNpm(plan.npmArgs, { cwd: plan.cwd, stdio: 'inherit' }); }
261
- catch { fail('npm update failed. Existing project state and installed skills were not intentionally removed.'); }
295
+ try {
296
+ repairDivergentProjectInstall(plan);
297
+ runNpm(plan.npmArgs, { cwd: plan.cwd, stdio: 'inherit' });
298
+ } catch { fail('npm update failed. Existing project state and installed skills were not intentionally removed.'); }
262
299
 
263
300
  if (!fs.existsSync(path.join(plan.packageRoot, 'package.json'))) fail(`updated package not found at ${plan.packageRoot}.`);
264
301
  const latest = JSON.parse(fs.readFileSync(path.join(plan.packageRoot, 'package.json'), 'utf8'));