afterimage-cli 0.2.3 → 0.2.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4
4
+
5
+ - **One command to install.** `npx afterimage-cli init` now does the whole setup: init installs afterimage-cli in the project as a dev dependency, at its own version, with the package manager your lockfile names (npm, pnpm, Yarn or Bun). A plain HTML folder no longer needs `npm init -y` first, since init creates `package.json` before the install. A project that already lists afterimage-cli installs nothing, and a failed install leaves the rest of the setup in place and prints the command to finish it. The setup prompt and the skill use the one command too.
6
+
3
7
  ## 0.2.3
4
8
 
5
9
  - **Scroll sync covers panels.** In an app that scrolls a panel instead of the page, Scroll synced in side by side, the wipe and holding B only moved the page, which never scrolls, so the two versions drifted apart. Now each panel follows the same panel in the other version, even when the page around it changed between versions.
package/README.md CHANGED
@@ -11,11 +11,10 @@ Afterimage keeps every version of your prototype you save and serves them all fr
11
11
  Afterimage needs Node 22 or later. From your prototype's project root:
12
12
 
13
13
  ```bash
14
- npm install -D afterimage-cli
15
- npx afterimage init
14
+ npx afterimage-cli init
16
15
  ```
17
16
 
18
- A plain HTML folder with no `package.json` needs `npm init -y` first.
17
+ npx asks once to fetch the package. `init` installs it in the project as a dev dependency, with the package manager your lockfile names, and creates `package.json` first for a plain HTML folder that has none.
19
18
 
20
19
  `init` works out what your prototype is: a plain HTML folder, a Vite app, a Next.js static export, an Astro site, or SvelteKit with `@sveltejs/adapter-static`. It puts the Afterimage pill on your prototype and makes `npm run dev` start Afterimage beside your app. It also writes `afterimage.config.json`, lines in `.gitattributes` and `.gitignore`, and host config for Vercel and Netlify, then lists every file it changed. For Next.js, Astro and SvelteKit it prints the one import that adds the pill. When it can't tell which folder holds the prototype, it asks. You can also tell it:
21
20
 
@@ -13,13 +13,11 @@ The user works on the prototype, not in a terminal. Do the steps yourself and re
13
13
 
14
14
  When the user asks to add Afterimage, or to track the prototype's versions, do all of this:
15
15
 
16
- 1. If the project has no `package.json`, run `npm init -y`.
17
- 2. Unless `package.json` already lists it, run `npm install --save-dev afterimage-cli`.
18
- 3. Run `npx afterimage init --agent claude --no-input` if you are Claude Code, or `npx afterimage init --agent cursor --no-input` if you are Cursor. Handle what it prints as "Set up" below says.
19
- 4. Init puts the pill on plain HTML pages and in a Vite app's `index.html` itself. For Next.js, Astro and SvelteKit it prints a note that starts with `Add the Afterimage pill to your app`. Make that change yourself, exactly as the note says.
20
- 5. Unless `afterimage/versions.json` already lists a version, run `npx afterimage save "First version"`.
21
- 6. Start `npm run dev` as a background process. If you started the project's dev server earlier in this session, stop that one first: this is the same server, with Afterimage beside it. Wait for the line that starts with `Afterimage is running at`.
22
- 7. Tell the user where the prototype is running and that the pill at its bottom saves a version and shows them all. The address is the app dev server's own (such as Vite's `Local:` line), or the one after `Your prototype is at` for plain HTML.
16
+ 1. Run `npx --yes afterimage-cli init --agent claude --no-input` if you are Claude Code, or `npx --yes afterimage-cli init --agent cursor --no-input` if you are Cursor. It installs afterimage-cli in the project unless `package.json` already lists it, and creates `package.json` when there is none. Handle what it prints as "Set up" below says.
17
+ 2. Init puts the pill on plain HTML pages and in a Vite app's `index.html` itself. For Next.js, Astro and SvelteKit it prints a note that starts with `Add the Afterimage pill to your app`. Make that change yourself, exactly as the note says.
18
+ 3. Unless `afterimage/versions.json` already lists a version, run `npx afterimage save "First version"`.
19
+ 4. Start `npm run dev` as a background process. If you started the project's dev server earlier in this session, stop that one first: this is the same server, with Afterimage beside it. Wait for the line that starts with `Afterimage is running at`.
20
+ 5. Tell the user where the prototype is running and that the pill at its bottom saves a version and shows them all. The address is the app dev server's own (such as Vite's `Local:` line), or the one after `Your prototype is at` for plain HTML.
23
21
 
24
22
  ## Commands
25
23
 
@@ -13,13 +13,11 @@ The user works on the prototype, not in a terminal. Do the steps yourself and re
13
13
 
14
14
  When the user asks to add Afterimage, or to track the prototype's versions, do all of this:
15
15
 
16
- 1. If the project has no `package.json`, run `npm init -y`.
17
- 2. Unless `package.json` already lists it, run `npm install --save-dev afterimage-cli`.
18
- 3. Run `npx afterimage init --agent claude --no-input` if you are Claude Code, or `npx afterimage init --agent cursor --no-input` if you are Cursor. Handle what it prints as "Set up" below says.
19
- 4. Init puts the pill on plain HTML pages and in a Vite app's `index.html` itself. For Next.js, Astro and SvelteKit it prints a note that starts with `Add the Afterimage pill to your app`. Make that change yourself, exactly as the note says.
20
- 5. Unless `afterimage/versions.json` already lists a version, run `npx afterimage save "First version"`.
21
- 6. Start `npm run dev` as a background process. If you started the project's dev server earlier in this session, stop that one first: this is the same server, with Afterimage beside it. Wait for the line that starts with `Afterimage is running at`.
22
- 7. Tell the user where the prototype is running and that the pill at its bottom saves a version and shows them all. The address is the app dev server's own (such as Vite's `Local:` line), or the one after `Your prototype is at` for plain HTML.
16
+ 1. Run `npx --yes afterimage-cli init --agent claude --no-input` if you are Claude Code, or `npx --yes afterimage-cli init --agent cursor --no-input` if you are Cursor. It installs afterimage-cli in the project unless `package.json` already lists it, and creates `package.json` when there is none. Handle what it prints as "Set up" below says.
17
+ 2. Init puts the pill on plain HTML pages and in a Vite app's `index.html` itself. For Next.js, Astro and SvelteKit it prints a note that starts with `Add the Afterimage pill to your app`. Make that change yourself, exactly as the note says.
18
+ 3. Unless `afterimage/versions.json` already lists a version, run `npx afterimage save "First version"`.
19
+ 4. Start `npm run dev` as a background process. If you started the project's dev server earlier in this session, stop that one first: this is the same server, with Afterimage beside it. Wait for the line that starts with `Afterimage is running at`.
20
+ 5. Tell the user where the prototype is running and that the pill at its bottom saves a version and shows them all. The address is the app dev server's own (such as Vite's `Local:` line), or the one after `Your prototype is at` for plain HTML.
23
21
 
24
22
  ## Commands
25
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "afterimage-cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "A version history for prototypes, deployed with the prototype itself.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,3 +1,4 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import readline from 'node:readline/promises';
@@ -349,6 +350,52 @@ async function planAgent(cwd, name, { isTTY, ask }) {
349
350
  };
350
351
  }
351
352
 
353
+ // "npx afterimage-cli init" runs from npx's cache, so the project gets the package too, at the version that is
354
+ // running: npm run dev, save and build all run it from node_modules. It goes in with the package manager the project's
355
+ // lockfile names, after every other step, since the install writes package.json too.
356
+ const { version } = JSON.parse(await fs.readFile(new URL('../../../package.json', import.meta.url), 'utf8'));
357
+ const installers = [['pnpm-lock.yaml', 'pnpm add -D'], ['yarn.lock', 'yarn add -D'], ['bun.lock', 'bun add -d'], ['bun.lockb', 'bun add -d']];
358
+
359
+ // The package manager's own output only shows when the install fails, as its last lines.
360
+ function installInTerminal(command, cwd) {
361
+ return new Promise((resolve, reject) => {
362
+ const child = spawn(command, { shell: true, cwd, stdio: ['ignore', 'pipe', 'pipe'] });
363
+ const output = [];
364
+ child.stdout.on('data', chunk => output.push(chunk));
365
+ child.stderr.on('data', chunk => output.push(chunk));
366
+ child.on('error', error => reject(new Error(`The install could not start: ${error.message}.`)));
367
+ child.on('close', code => {
368
+ if (code === 0) return resolve();
369
+ const said = Buffer.concat(output).toString().trim().split(/\r?\n/).slice(-12).join('\n');
370
+ reject(new Error(`${said ? `${said}\n` : ''}The install failed (exit code ${code}).`));
371
+ });
372
+ });
373
+ }
374
+
375
+ async function planInstall(cwd, { manifest }, { stdout, install }) {
376
+ if ('afterimage-cli' in { ...manifest.dependencies, ...manifest.devDependencies }) return { change: null, notes: [], apply: null };
377
+ let installer = 'npm install -D';
378
+ for (const [lockfile, command] of installers) {
379
+ if (await readText(path.join(cwd, lockfile)) !== null) {
380
+ installer = command;
381
+ break;
382
+ }
383
+ }
384
+ const command = `${installer} afterimage-cli@${version}`;
385
+ return {
386
+ change: `Installed afterimage-cli ${version} as a dev dependency`,
387
+ notes: [],
388
+ apply: async () => {
389
+ stdout.write(`Installing afterimage-cli ${version}...\n`);
390
+ try {
391
+ await install(command, cwd);
392
+ } catch (error) {
393
+ throw new Error(`${error.message}\nEverything else is set up. Install Afterimage yourself to finish: ${command}`);
394
+ }
395
+ }
396
+ };
397
+ }
398
+
352
399
  export async function run(args, ctx) {
353
400
  const { cwd, stdout, stderr } = ctx;
354
401
 
@@ -381,7 +428,7 @@ export async function run(args, ctx) {
381
428
 
382
429
  // Plan first, write after. package.json is read before the config step so a bad one stops init before any question,
383
430
  // and detection (which reads package.json) still sees the project as it was.
384
- let plans;
431
+ let plans, agentPlan;
385
432
  try {
386
433
  const packageFile = await readPackage(cwd);
387
434
  const configPlan = await planConfig(cwd, flags, prompt);
@@ -394,7 +441,8 @@ export async function run(args, ctx) {
394
441
  await planLines(cwd, '.gitignore', ['.afterimage/', `${configPlan.config.history}/.afterimage-*`, 'node_modules/'], hasIgnore),
395
442
  await planVercel(cwd, prompt),
396
443
  await planNetlify(cwd),
397
- await planAgent(cwd, agent, prompt)
444
+ agentPlan = await planAgent(cwd, agent, prompt),
445
+ await planInstall(cwd, packageFile, { stdout, install: ctx.install ?? installInTerminal })
398
446
  ];
399
447
  } catch (error) {
400
448
  // Ctrl-C at a prompt. Nothing has been written yet, so stopping here is clean. 130 is the shell's code for SIGINT.
@@ -411,7 +459,7 @@ export async function run(args, ctx) {
411
459
  else stdout.write(notes.length ? 'No files changed. Still left for you:\n' : 'Everything is already set up. Nothing changed.\n');
412
460
  if (notes.length) stdout.write(`${changes.length ? '\n' : ''}${notes.join('\n\n')}\n`);
413
461
  // With --agent the copy is in place, and a fresh one needs loading; without it, say how to add it.
414
- const agentLine = agent ? plans.at(-1).change && agents[agent].loads : skillHint;
462
+ const agentLine = agent ? agentPlan.change && agents[agent].loads : skillHint;
415
463
  const dev = (await readPackage(cwd)).manifest.scripts?.dev;
416
464
  stdout.write(`\n${agentLine ? `${agentLine}\n` : ''}${nextStep(typeof dev === 'string' && dev.trim().startsWith(devWrap))}\n`);
417
465
  return 0;