gipity 1.0.408 → 1.0.409

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.
@@ -144,7 +144,7 @@ function buildLocalPayload(rootDir) {
144
144
  return { name: path.basename(rootDir), files };
145
145
  }
146
146
  export const addCommand = new Command('add')
147
- .description('Add a template (scaffold an app) or a kit (reusable building block) to the project. Pass ./path/to/dir to install a local template directly.')
147
+ .description('Add a template or kit')
148
148
  .argument('[name]', 'Template/kit key, OR a local directory path (./, ~/, or /abs). Omit for help; use --list for just the catalog.')
149
149
  .option('--title <title>', 'App title - templates only (defaults to project name)')
150
150
  .option('--description <desc>', 'App description for meta tags - templates only')
@@ -21,7 +21,7 @@ function keyToIndex(key) {
21
21
  return code >= 0 && code < 26 ? code : -1;
22
22
  }
23
23
  export const approvalCommand = new Command('approval')
24
- .description('List, create, answer, or cancel approvals');
24
+ .description('Manage approvals');
25
25
  approvalCommand
26
26
  .command('list')
27
27
  .description('List pending approvals')
@@ -7,7 +7,8 @@ function collect(value, prev) {
7
7
  return [...prev, value];
8
8
  }
9
9
  export const emailCommand = new Command('email')
10
- .description('Send email from the Gipity platform (gipity@gipity.ai). Omit --to to self-send.')
10
+ .description('Send email')
11
+ .addHelpText('after', '\nSends from gipity@gipity.ai. Omit --to to self-send.')
11
12
  .requiredOption('--subject <subject>', 'Email subject')
12
13
  .requiredOption('--body <body>', 'Email body (plain text)')
13
14
  .option('--to <email>', 'Recipient (repeatable; omit for self-send)', collect, [])
@@ -3,7 +3,8 @@ import { get, post } from '../api.js';
3
3
  import { run, printList, printResult } from '../helpers/index.js';
4
4
  import { muted } from '../colors.js';
5
5
  export const gmailCommand = new Command('gmail')
6
- .description('Send, reply, search, or read via your Gmail (user channel - separate from `gipity email`)');
6
+ .description('Use your Gmail')
7
+ .addHelpText('after', '\nSend, reply, search, or read via your own Gmail (separate from `gipity email`).');
7
8
  gmailCommand
8
9
  .command('search <query...>')
9
10
  .description('Search your Gmail (standard Gmail search syntax)')
@@ -22,7 +22,8 @@ function resolveTools(forFlag) {
22
22
  return SUPPORTED_TOOLS.filter(t => requested.includes(t.key) || (!t.optIn && requested.includes('all')));
23
23
  }
24
24
  export const initCommand = new Command('init')
25
- .description('Link this directory to a Gipity project (writes primer files so your AI coding tool understands Gipity)')
25
+ .description('Link this directory to a project')
26
+ .addHelpText('after', '\nWrites CLAUDE.md/AGENTS.md primer files so your AI coding tool understands Gipity.')
26
27
  .argument('[name]', 'Project name/slug (defaults to current directory name)')
27
28
  .option('--agent <guid>', 'Agent GUID to use')
28
29
  .option('--for <tools>', `Which AI tool primer files to write (comma-separated). Default: all except aider (opt-in - it also writes .aider.conf.yml). Choices: ${TOOL_KEYS.join(', ')}, all`)
@@ -4,7 +4,8 @@ import { requireConfig } from '../config.js';
4
4
  import { error as clrError, bold, muted, success, warning as warn } from '../colors.js';
5
5
  import { run, printList } from '../helpers/index.js';
6
6
  export const jobCommand = new Command('job')
7
- .description('Long-running jobs - CPU sandbox or GPU via Modal (gpu-small=L4, gpu-medium=A10G, gpu-large=A100, gpu-huge=H100). Declared in gipity.yaml jobs: phase; submit + poll via subcommands below.');
7
+ .description('Run long jobs (CPU/GPU)')
8
+ .addHelpText('after', '\nCPU sandbox or GPU (gpu-small=L4, gpu-medium=A10G, gpu-large=A100, gpu-huge=H100). Declared in the gipity.yaml jobs: phase; submit and poll via the subcommands below.');
8
9
  jobCommand
9
10
  .command('list')
10
11
  .description('List jobs')
@@ -34,6 +34,7 @@ export const loginCommand = new Command('login')
34
34
  process.exit(1);
35
35
  }
36
36
  await publicPost('/auth/login', { email });
37
+ console.log('');
37
38
  console.log('Check your email for a 6-digit code.');
38
39
  code = await prompt('Code: ');
39
40
  await verify(email, code);
@@ -4,7 +4,8 @@ import { resolveProjectContext } from '../config.js';
4
4
  import { bold, muted, success, warning } from '../colors.js';
5
5
  import { run } from '../helpers/index.js';
6
6
  export const notifyCommand = new Command('notify')
7
- .description('Gipity Notify (web push): send a test notification or inspect subscriptions');
7
+ .description('Web push notifications')
8
+ .addHelpText('after', '\nGipity Notify: send a test notification or inspect subscriptions.');
8
9
  notifyCommand
9
10
  .command('test')
10
11
  .description('Send a test push notification to yourself, a user, or everyone')
@@ -10,7 +10,7 @@ import { pageFetchCommand } from './page-fetch.js';
10
10
  // top-level surface lean and makes the siblings discoverable via `page --help`.
11
11
  // `inspect` is the rendered DOM (browser); `fetch` is the raw asset (plain HTTP).
12
12
  export const pageCommand = new Command('page')
13
- .description('Inspect, evaluate, screenshot, multi-client test, and verify raw files of web pages (page inspect | eval | screenshot | test | fetch)')
13
+ .description('Inspect web pages')
14
14
  .addCommand(pageInspectCommand)
15
15
  .addCommand(pageEvalCommand)
16
16
  .addCommand(pageScreenshotCommand)
@@ -19,7 +19,8 @@ function renderStatus(status) {
19
19
  }
20
20
  }
21
21
  export const paymentsCommand = new Command('payments')
22
- .description('Connect Stripe so your app can charge its users (one-time + subscriptions)');
22
+ .description('Charge your users via Stripe')
23
+ .addHelpText('after', '\nAccept one-time purchases and subscriptions. Set up with `gipity payments connect`.');
23
24
  paymentsCommand
24
25
  .command('connect')
25
26
  .description('Start (or resume) Stripe onboarding for this app — prints a link to finish in your browser')
@@ -31,7 +31,7 @@ function renderLimits(limits, indent = '') {
31
31
  }
32
32
  }
33
33
  export const planCommand = new Command('plan')
34
- .description('Show your current subscription plan and limits')
34
+ .description('Show your plan')
35
35
  .option('--json', 'Output as JSON')
36
36
  .action((opts) => run('Plan', async () => {
37
37
  const [limitsRes, plansRes] = await Promise.all([
@@ -7,7 +7,7 @@ import { run } from '../helpers/index.js';
7
7
  import { confirm } from '../utils.js';
8
8
  import { createProgressReporter, withSpinner } from '../progress.js';
9
9
  export const removeCommand = new Command('remove')
10
- .description('Remove an installed kit from the project (inverse of `gipity add <kit>`).')
10
+ .description('Remove a kit')
11
11
  .argument('<kit>', 'Kit key/directory under src/packages/ to remove')
12
12
  .option('-y, --yes', 'Skip the confirmation prompt')
13
13
  .option('--json', 'Output as JSON')
@@ -12,7 +12,8 @@ async function scopeQuery(opts) {
12
12
  return `scope=project&app_guid=${config.projectGuid}`;
13
13
  }
14
14
  export const secretsCommand = new Command('secrets')
15
- .description('Encrypted secrets (API keys, tokens) for your app — read in functions via secrets.get("NAME")');
15
+ .description('Manage app secrets')
16
+ .addHelpText('after', '\nEncrypted API keys and tokens for your app, read in functions via secrets.get("NAME").');
16
17
  secretsCommand
17
18
  .command('list')
18
19
  .alias('ls')
@@ -20,7 +20,8 @@ const SERVICES = [
20
20
  { name: 'location/geocode', method: 'POST', desc: 'Reverse geocode ({ lat, lon })' },
21
21
  ];
22
22
  export const serviceCommand = new Command('service')
23
- .description('Call a Gipity app service (LLM, image, music, TTS, ...)');
23
+ .description('Call an app service')
24
+ .addHelpText('after', '\nCall a Gipity app service: LLM, image, music, TTS, and more.');
24
25
  serviceCommand
25
26
  .command('list')
26
27
  .description('List callable app services')
@@ -3,7 +3,8 @@ import { sync } from '../sync.js';
3
3
  import { createProgressReporter } from '../progress.js';
4
4
  import { error as clrError, muted } from '../colors.js';
5
5
  export const syncCommand = new Command('sync')
6
- .description('Sync files (a .gipityignore at the project root excludes paths, gitignore-style)')
6
+ .description('Sync files')
7
+ .addHelpText('after', '\nAdd a .gipityignore at the project root to exclude paths (gitignore-style).')
7
8
  .option('--plan', 'Print the plan without applying any changes')
8
9
  .option('--force', 'Bypass the bulk-deletion guard')
9
10
  .option('--prune', 'Remove files that exist on Gipity but not locally (applies the bulk deletes the guard defers)')
@@ -140,7 +140,8 @@ const analyzeCommand = new Command('analyze')
140
140
  // Parent namespace. One capability today (analyze); namespaced for a lean
141
141
  // top-level surface and room for future text operations.
142
142
  export const textCommand = new Command('text')
143
- .description(`Deterministic text analysis - char/word counts, frequency, occurrences (${brand('text analyze')})`)
143
+ .description('Analyze text')
144
+ .addHelpText('after', `\nDeterministic char/word counts, frequency, and occurrences (${brand('text analyze')}). Local, no network.`)
144
145
  .addCommand(analyzeCommand);
145
146
  textCommand.action(() => {
146
147
  textCommand.help();
@@ -3,7 +3,8 @@ import { get, post, del } from '../api.js';
3
3
  import { bold, muted, success, warning } from '../colors.js';
4
4
  import { run, printList } from '../helpers/index.js';
5
5
  export const tokenCommand = new Command('token')
6
- .description('Manage agent API tokens (gip_at_*) for headless agents and CI');
6
+ .description('Manage API tokens')
7
+ .addHelpText('after', '\nLong-lived agent API tokens (gip_at_*) for headless agents and CI.');
7
8
  const fmtDate = (d) => (d ? new Date(d).toLocaleDateString() : 'never');
8
9
  tokenCommand
9
10
  .command('create')
package/dist/config.js CHANGED
@@ -134,8 +134,8 @@ export async function resolveProjectContext(opts) {
134
134
  }
135
135
  const agents = await get(`/projects/${match.short_guid}/agents`);
136
136
  const accountSlug = await getAccountSlug();
137
- console.error(dim(`→ One-off mode: targeting project "${match.slug}" (--project override).`));
138
- console.error(dim(`→ Files are not synced - outputs will also be downloaded to ./ for you.`));
137
+ console.error(dim(`→ (project: ${match.slug} · no file sync)`));
138
+ console.error('');
139
139
  return {
140
140
  config: {
141
141
  projectGuid: match.short_guid,
@@ -163,8 +163,8 @@ export async function resolveProjectContext(opts) {
163
163
  console.error('Could not resolve your Home project - please contact support.');
164
164
  process.exit(1);
165
165
  }
166
- console.error(dim(`→ One-off mode: no .gipity.json in cwd, using your Home project on the server.`));
167
- console.error(dim(`→ Files are not synced - outputs will also be downloaded to ./ for you.`));
166
+ console.error(dim(`→ (project: ${res.data.projectName} · no file sync)`));
167
+ console.error('');
168
168
  return {
169
169
  config: {
170
170
  projectGuid: res.data.projectGuid,
@@ -3,16 +3,38 @@
3
3
  * can close a frame even when a command ends via process.exit() and so skips
4
4
  * the postAction hook. */
5
5
  let frameOpen = false;
6
+ /** Count of newlines currently at the tail of everything written to stdout. Kept
7
+ * live by wrapping stdout.write so the frame can close to EXACTLY one trailing
8
+ * blank line instead of blindly appending one - the blind append doubled the
9
+ * blank whenever a command's own output already ended in a newline (e.g. an
10
+ * agent reply printed with a trailing '\n'). */
11
+ let trailingNewlines = 0;
12
+ function trackTrailing(chunk) {
13
+ const s = typeof chunk === 'string'
14
+ ? chunk
15
+ : (chunk && typeof chunk.toString === 'function' ? String(chunk) : '');
16
+ if (!s.length)
17
+ return;
18
+ const run = /\n*$/.exec(s)?.[0].length ?? 0;
19
+ // A chunk that is all newlines extends the current run; otherwise it resets it
20
+ // to just this chunk's own trailing run.
21
+ trailingNewlines = run === s.length ? trailingNewlines + run : run;
22
+ }
6
23
  /**
7
24
  * Bracket every human (non-JSON) command's stdout with exactly one leading and
8
25
  * one trailing blank line, so output is visually separated from the shell
9
26
  * prompt and consistent across commands. JSON output is left untouched so it
10
27
  * stays pipe/parse-clean.
11
28
  *
29
+ * The trailing blank is added by TOP-UP, not blind append: one blank line means
30
+ * the stream ends in two newlines (the content's own line terminator plus one
31
+ * empty line), so we only write the newlines still missing. That keeps the
32
+ * boundary at exactly one blank whether the command ended its output with no
33
+ * newline, one, or an accidental extra.
34
+ *
12
35
  * Centralizing the frame here is what lets individual commands stop printing
13
- * their own leading/trailing blank lines (and stops them doubling up at the
14
- * boundaries). Call once on the root program; Commander runs these lifecycle
15
- * hooks for the actual subcommand action too.
36
+ * their own leading/trailing blank lines. Call once on the root program;
37
+ * Commander runs these lifecycle hooks for the actual subcommand action too.
16
38
  */
17
39
  export function installOutputFrame(program) {
18
40
  // Only decorate a real terminal. When stdout is piped or redirected (a relay
@@ -20,6 +42,21 @@ export function installOutputFrame(program) {
20
42
  // colors.ts uses for ANSI. This keeps headless `claude -p` stdout empty.
21
43
  if (!process.stdout.isTTY)
22
44
  return;
45
+ // Wrap stdout.write to keep `trailingNewlines` current for every byte the
46
+ // command emits (console.log, spinner, colors all route through here).
47
+ const origWrite = process.stdout.write.bind(process.stdout);
48
+ process.stdout.write = (chunk, ...rest) => {
49
+ trackTrailing(chunk);
50
+ return origWrite(chunk, ...rest);
51
+ };
52
+ const closeFrame = () => {
53
+ if (!frameOpen)
54
+ return;
55
+ frameOpen = false;
56
+ const need = Math.max(0, 2 - trailingNewlines);
57
+ if (need > 0)
58
+ origWrite('\n'.repeat(need));
59
+ };
23
60
  program.hook('preAction', (_thisCommand, actionCommand) => {
24
61
  const opts = actionCommand.optsWithGlobals
25
62
  ? actionCommand.optsWithGlobals()
@@ -29,18 +66,10 @@ export function installOutputFrame(program) {
29
66
  process.stdout.write('\n');
30
67
  frameOpen = true;
31
68
  });
32
- program.hook('postAction', () => {
33
- if (frameOpen) {
34
- process.stdout.write('\n');
35
- frameOpen = false;
36
- }
37
- });
69
+ program.hook('postAction', closeFrame);
38
70
  // Commands that finish via process.exit() never reach postAction; close the
39
71
  // frame from the exit handler instead (sync write, fires on every exit path).
40
- process.on('exit', () => {
41
- if (frameOpen)
42
- process.stdout.write('\n');
43
- });
72
+ process.on('exit', closeFrame);
44
73
  }
45
74
  /**
46
75
  * Print data as JSON or formatted text.
@@ -52,7 +52,7 @@ export function bootstrap(version, quiet = false) {
52
52
  state.installedVersion = version;
53
53
  writeState(state);
54
54
  if (!quiet)
55
- process.stderr.write(`Done.\n\n`);
55
+ process.stderr.write(`Done.\n`);
56
56
  return true;
57
57
  }
58
58
  export { LOCAL_PKG_DIR };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gipity",
3
- "version": "1.0.408",
3
+ "version": "1.0.409",
4
4
  "description": "The full-stack platform tuned for AI agents. Database, storage, auth, functions, deploy, and drop-in kits - all agent-tuned. Pair with Claude Code or use standalone.",
5
5
  "bin": {
6
6
  "gipity": "dist/updater/shim.js",
@@ -1,52 +0,0 @@
1
- /**
2
- * Shared coding guidelines for Gipity-hosted projects.
3
- * Single source of truth used by:
4
- * - server/src/services/skills/web-app-basics.ts (web CLI agent)
5
- * - tools/gipity-cli/src/setup.ts (CLAUDE.md template for Claude Code)
6
- *
7
- * The CLI build copies this file before compiling (see justfile cli-build).
8
- */
9
- export const CODING_GUIDELINES = `## File Structure
10
- - **Use src/ convention**: All app files live under \`src/\` — \`src/index.html\`, \`src/css/styles.css\`, \`src/js/main.js\`, \`src/images/\`
11
- - **Separate files**: Split into \`index.html\`, \`styles.css\`, and \`app.js\` (or \`main.js\`). Never inline large blocks of CSS or JS in HTML.
12
- - If the app grows, organize into folders: \`src/css/\`, \`src/js/\`, \`src/assets/\`, \`src/sounds/\`, \`src/images/\`, etc.
13
- - **Use subfolders — don't flatten**: Reference assets from their folders (e.g. \`sounds/click.ogg\`, \`images/logo.png\`). Never copy files to the root just for convenience — deployed apps serve the full directory tree.
14
- - Keep \`index.html\` clean — it should be structure/markup, not behavior or styling
15
-
16
- ## HTML
17
- - Use semantic elements: \`<header>\`, \`<nav>\`, \`<main>\`, \`<section>\`, \`<footer>\`, \`<article>\`
18
- - Always include \`<meta name="viewport" content="width=device-width, initial-scale=1.0">\`
19
- - Add a proper \`<title>\` and favicon link
20
- - Unless the user specifies a different CSS framework, include Water.css for automatic styling: \`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">\`
21
- - Water.css styles semantic HTML automatically (buttons, tables, forms, nav, cards) — no classes needed. It supports dark/light themes automatically. Add custom CSS on top for app-specific tweaks.
22
-
23
- ## CSS
24
- - When using Water.css, it handles base styling, resets, and typography — don't duplicate what it provides
25
- - Water.css exposes CSS variables for theming — override them in \`:root\` for custom colors/fonts
26
- - Use CSS custom properties (variables) for app-specific colors, spacing, and fonts
27
- - Add smooth transitions on interactive elements (buttons, links, hover states)
28
-
29
- ## JavaScript
30
- - Use \`const\`/\`let\`, arrow functions, template literals, and modern ES6+ syntax
31
- - Wait for DOM: wrap in \`DOMContentLoaded\` or place script at end of body
32
- - Keep functions small and focused
33
- - Use \`addEventListener\` — never inline \`onclick\` attributes in HTML
34
-
35
- ## Code Quality
36
- - **Keep files under ~400 lines** unless the content genuinely requires it (e.g. a long data table, template string, or config object). When logic grows beyond that, split into focused modules (e.g. \`utils.js\`, \`api.js\`, \`ui.js\`).
37
- - **Don't duplicate code.** If the same logic appears twice, extract it into a shared function. Before writing a new helper, check if one already exists or could be extended.
38
- - **One responsibility per file.** A file that handles both UI rendering and API calls should be split.
39
- - **Name things clearly.** Functions, variables, and files should describe what they do — no \`temp\`, \`data2\`, \`stuff.js\`.
40
- - **Prefer simple, readable code** over clever code that hides bugs. Flat over nested — use early returns, avoid deep nesting.
41
- - **Centralize configuration.** App settings, API URLs, feature flags, and magic numbers should live in a dedicated config file (e.g. \`config.js\` or \`constants.js\`), not scattered across the codebase.
42
- - **Write utility functions** for repeated operations (formatting, validation, API calls). Keep them in a \`utils.js\` or \`helpers.js\` file. Small, pure functions are easy to test and reuse.
43
-
44
- ## Testing
45
- - **Write tests for new functions** — especially utility/helper functions. Cover the happy path and edge cases (empty input, null, boundary values).
46
- - **Don't mock unless absolutely required.** Tests should exercise real code paths. Only mock external paid services (APIs that cost money per call).
47
- - **E2E tests should hit real infrastructure** (real API, real DB) — just clean up test data when done.
48
- - **Test file naming**: \`*.test.js\` for unit tests, \`*.e2e.test.js\` for end-to-end tests.
49
-
50
- ## Deployment
51
- - **src/ detection**: If a \`src/\` directory exists, only \`src/\` is deployed. Otherwise the full project root is deployed.`;
52
- //# sourceMappingURL=coding-guidelines.js.map
@@ -1,84 +0,0 @@
1
- import { Command } from 'commander';
2
- import { post } from '../api.js';
3
- import { resolveProjectContext } from '../config.js';
4
- import { formatSize } from '../utils.js';
5
- import { brand, bold, error as clrError, warning, muted, info } from '../colors.js';
6
- import { run } from '../helpers/index.js';
7
- function shortUrl(url) {
8
- try {
9
- const u = new URL(url);
10
- return u.pathname + u.search;
11
- }
12
- catch {
13
- return url.length > 60 ? url.slice(-60) : url;
14
- }
15
- }
16
- export const browserCommand = new Command('browser')
17
- .description('Inspect a URL: console errors, performance, failed resources')
18
- .argument('<url>', 'URL to inspect')
19
- .option('--wait <ms>', 'Wait before capture in ms', '3000')
20
- .option('--json', 'Output as JSON')
21
- .action((url, opts) => run('Browser inspect', async () => {
22
- const { config } = await resolveProjectContext();
23
- const waitMs = parseInt(opts.wait, 10) || 3000;
24
- const res = await post(`/projects/${config.projectGuid}/browser/inspect`, { url, waitMs });
25
- const b = res.data;
26
- if (opts.json) {
27
- console.log(JSON.stringify(b));
28
- return;
29
- }
30
- const timing = b.timing || { ttfb: 0, domReady: 0, load: 0 };
31
- // ── Page Info ──
32
- console.log(`\n${brand('Inspecting')} ${bold(b.url || url)}`);
33
- console.log(` ${muted('Title:')} ${b.title || '(none)'}`);
34
- console.log(` ${muted('Elements:')} ${b.elementCount || 0}`);
35
- console.log(` ${muted('Page weight:')} ${info(formatSize(b.totalBytes || 0))}`);
36
- // ── Timing ──
37
- console.log(`\n ${bold('Timing:')}`);
38
- console.log(` ${muted('TTFB:')} ${timing.ttfb}ms`);
39
- console.log(` ${muted('DOM ready:')} ${timing.domReady}ms`);
40
- console.log(` ${muted('Load:')} ${timing.load}ms`);
41
- if (b.lcp) {
42
- console.log(` LCP: ${b.lcp.time}ms (${b.lcp.element}${b.lcp.url ? ' ' + shortUrl(b.lcp.url) : ''})`);
43
- }
44
- // ── Console ──
45
- if (b.console?.length > 0) {
46
- console.log(`\n ${bold('Console')} ${muted(`(${b.console.length})`)}:`);
47
- for (const line of b.console) {
48
- console.log(` ${warning(line)}`);
49
- }
50
- }
51
- else {
52
- console.log(`\n ${bold('Console:')} ${muted('(clean)')}`);
53
- }
54
- // ── Failed Resources ──
55
- if (b.failedResources?.length > 0) {
56
- console.log(`\n ${clrError(`Failed resources (${b.failedResources.length}):`)}`);
57
- for (const r of b.failedResources) {
58
- console.log(` ${clrError(r)}`);
59
- }
60
- }
61
- // ── Render Blocking ──
62
- if (b.renderBlocking?.length > 0) {
63
- console.log(`\n ${warning(`Render-blocking (${b.renderBlocking.length}):`)}`);
64
- for (const r of b.renderBlocking) {
65
- console.log(` ${shortUrl(r)}`);
66
- }
67
- }
68
- // ── Large Resources ──
69
- if (b.largeResources?.length > 0) {
70
- console.log(`\n ${warning(`Large resources >100KB (${b.largeResources.length}):`)}`);
71
- for (const r of b.largeResources) {
72
- console.log(` ${info(formatSize(r.size).padEnd(10))} ${muted(r.type.padEnd(8))} ${shortUrl(r.url)}`);
73
- }
74
- }
75
- // ── Oversized Images ──
76
- if (b.oversizedImages?.length > 0) {
77
- console.log(`\n ${warning(`Oversized images (${b.oversizedImages.length}):`)}`);
78
- for (const img of b.oversizedImages) {
79
- console.log(` ${img.natural} served, ${img.displayed} displayed — ${shortUrl(img.src)}`);
80
- }
81
- }
82
- console.log('');
83
- }));
84
- //# sourceMappingURL=browser.js.map
@@ -1,68 +0,0 @@
1
- import { Command } from 'commander';
2
- import { get, post } from '../api.js';
3
- import { requireConfig } from '../config.js';
4
- import { syncDown } from '../sync.js';
5
- import { formatAge } from '../utils.js';
6
- import { error as clrError, muted, success } from '../colors.js';
7
- export const checkpointCommand = new Command('checkpoint')
8
- .description('Manage file checkpoints (snapshots for undo/restore)');
9
- checkpointCommand
10
- .command('list')
11
- .description('List checkpoints')
12
- .option('--limit <n>', 'Max results', '20')
13
- .option('--json', 'Output as JSON')
14
- .action(async (opts) => {
15
- try {
16
- const config = requireConfig();
17
- const limit = parseInt(opts.limit, 10) || 20;
18
- const res = await get(`/projects/${config.projectGuid}/checkpoints?limit=${limit}`);
19
- if (opts.json) {
20
- console.log(JSON.stringify(res.data));
21
- }
22
- else {
23
- if (res.data.length === 0) {
24
- console.log('No checkpoints.');
25
- }
26
- else {
27
- for (const cp of res.data) {
28
- const age = formatAge(cp.created_at);
29
- const branch = cp.branched ? ' (branched)' : '';
30
- console.log(` ${muted(cp.guid)} ${cp.label || muted('(auto)')} ${cp.fileCount} files ${muted(age)}${branch}`);
31
- }
32
- }
33
- }
34
- }
35
- catch (err) {
36
- console.error(clrError(`List failed: ${err.message}`));
37
- process.exit(1);
38
- }
39
- });
40
- checkpointCommand
41
- .command('restore <guid>')
42
- .description('Restore files to a checkpoint (creates a backup checkpoint first)')
43
- .option('--json', 'Output as JSON')
44
- .action(async (guid, opts) => {
45
- try {
46
- const config = requireConfig();
47
- const res = await post(`/projects/${config.projectGuid}/checkpoints/restore`, {
48
- checkpoint_guid: guid,
49
- });
50
- // Sync down restored files (confirm deletions — restore may remove files)
51
- const syncResult = await syncDown({ confirmDeletions: true });
52
- if (opts.json) {
53
- console.log(JSON.stringify({ ...res.data, synced: syncResult.pulled }));
54
- }
55
- else {
56
- console.log(success(`Restored to ${res.data.restoredTo}`));
57
- console.log(`Backup created: ${res.data.backupCheckpoint}`);
58
- if (syncResult.pulled > 0) {
59
- console.log(`Pulled ${syncResult.pulled} files to local.`);
60
- }
61
- }
62
- }
63
- catch (err) {
64
- console.error(clrError(`Restore failed: ${err.message}`));
65
- process.exit(1);
66
- }
67
- });
68
- //# sourceMappingURL=checkpoint.js.map