standout 0.6.0 → 0.6.1

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 +6 -4
  2. package/dist/cli.js +59 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,10 +14,12 @@ to build a full profile interactively.
14
14
 
15
15
  ## Runs monthly, automatically
16
16
 
17
- After a full run, standout schedules a monthly refresh (1st of each month) so
18
- your stats never decay — local session logs only cover ~30 days, and the
19
- refresh keeps your full history intact. Uses launchd on macOS and cron on
20
- Linux; not available on Windows.
17
+ After a full run, standout schedules a monthly refresh so your stats never
18
+ decay — local session logs only cover ~30 days, and the refresh keeps your
19
+ full history intact. It fires at the first hour your machine is awake in each
20
+ new month (an hourly launchd/cron tick guarded by a month stamp in
21
+ `~/.standout/last-refresh`, so sleeping through the 1st never skips a month).
22
+ Uses launchd on macOS and cron on Linux; not available on Windows.
21
23
 
22
24
  ```bash
23
25
  npx standout schedule status # is it on? command, log path, last run
package/dist/cli.js CHANGED
@@ -5203,7 +5203,14 @@ function escapeXml(text) {
5203
5203
  function unescapeXml(text) {
5204
5204
  return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
5205
5205
  }
5206
- function buildLaunchdPlist(command, logPath) {
5206
+ function buildGuardedCommand(runCommand) {
5207
+ return `STAMP="$HOME/.standout/last-refresh"; MONTH=$(date +%Y-%m); [ "$(cat "$STAMP" 2>/dev/null)" = "$MONTH" ] || { mkdir -p "$HOME/.standout"; STANDOUT_SCHEDULED=1 ${runCommand} && echo "$MONTH" > "$STAMP"; }`;
5208
+ }
5209
+ function extractRunCommand(guarded) {
5210
+ const m = guarded.match(/STANDOUT_SCHEDULED=1 (.*?) && echo "\$MONTH"/);
5211
+ return m ? m[1] : null;
5212
+ }
5213
+ function buildLaunchdPlist(guardedCommand, logPath) {
5207
5214
  return `<?xml version="1.0" encoding="UTF-8"?>
5208
5215
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
5209
5216
  <plist version="1.0">
@@ -5213,22 +5220,18 @@ function buildLaunchdPlist(command, logPath) {
5213
5220
  <array>
5214
5221
  <string>/bin/zsh</string>
5215
5222
  <string>-lc</string>
5216
- <string>${escapeXml(command)}</string>
5223
+ <string>${escapeXml(guardedCommand)}</string>
5217
5224
  </array>
5218
- <key>StartCalendarInterval</key>
5219
- <dict>
5220
- <key>Day</key><integer>1</integer>
5221
- <key>Hour</key><integer>10</integer>
5222
- <key>Minute</key><integer>0</integer>
5223
- </dict>
5225
+ <key>StartInterval</key><integer>3600</integer>
5224
5226
  <key>StandardOutPath</key><string>${escapeXml(logPath)}</string>
5225
5227
  <key>StandardErrorPath</key><string>${escapeXml(logPath)}</string>
5226
5228
  </dict>
5227
5229
  </plist>
5228
5230
  `;
5229
5231
  }
5230
- function buildCronLine(command, logPath) {
5231
- return `0 10 1 * * /bin/bash -lc '${command}' >> "${logPath}" 2>&1 ${CRON_MARKER}`;
5232
+ function buildCronLine(guardedCommand, logPath) {
5233
+ const escaped = guardedCommand.replace(/%/g, "\\%");
5234
+ return `0 * * * * /bin/bash -lc '${escaped}' >> "${logPath}" 2>&1 ${CRON_MARKER}`;
5232
5235
  }
5233
5236
  function stripCronEntries(crontab) {
5234
5237
  return crontab.split("\n").filter((line) => !line.includes(CRON_MARKER)).join("\n");
@@ -5261,6 +5264,23 @@ function writeOptOut() {
5261
5264
  function clearOptOut() {
5262
5265
  rmSync2(optOutPath(), { force: true });
5263
5266
  }
5267
+ function refreshStampPath() {
5268
+ return join6(stateDir(), "last-refresh");
5269
+ }
5270
+ function readRefreshStamp() {
5271
+ try {
5272
+ return readFileSync6(refreshStampPath(), "utf8").trim() || null;
5273
+ } catch {
5274
+ return null;
5275
+ }
5276
+ }
5277
+ function writeRefreshStamp() {
5278
+ mkdirSync2(stateDir(), { recursive: true });
5279
+ const now = /* @__PURE__ */ new Date();
5280
+ const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
5281
+ writeFileSync(refreshStampPath(), `${month}
5282
+ `);
5283
+ }
5264
5284
  function launchctl(args) {
5265
5285
  const res = spawnSync2("launchctl", args, { encoding: "utf8" });
5266
5286
  return {
@@ -5286,10 +5306,11 @@ function writeCrontab(content) {
5286
5306
  }
5287
5307
  }
5288
5308
  function installJob(command) {
5309
+ const guarded = buildGuardedCommand(command);
5289
5310
  if (process.platform === "darwin") {
5290
5311
  const path2 = plistPath();
5291
5312
  mkdirSync2(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
5292
- writeFileSync(path2, buildLaunchdPlist(command, schedLogPath()));
5313
+ writeFileSync(path2, buildLaunchdPlist(guarded, schedLogPath()));
5293
5314
  launchctl(["bootout", `gui/${uid()}/${LAUNCHD_LABEL}`]);
5294
5315
  const res = launchctl(["bootstrap", `gui/${uid()}`, path2]);
5295
5316
  if (res.status !== 0) {
@@ -5301,10 +5322,11 @@ function installJob(command) {
5301
5322
  mkdirSync2(stateDir(), { recursive: true });
5302
5323
  const stripped = stripCronEntries(readCrontab());
5303
5324
  const base = stripped === "" || stripped.endsWith("\n") ? stripped : stripped + "\n";
5304
- writeCrontab(base + buildCronLine(command, schedLogPath()) + "\n");
5325
+ writeCrontab(base + buildCronLine(guarded, schedLogPath()) + "\n");
5305
5326
  }
5327
+ writeRefreshStamp();
5306
5328
  }
5307
- function installedCommand() {
5329
+ function installedShellCommand() {
5308
5330
  if (process.platform === "darwin") {
5309
5331
  if (!existsSync6(plistPath())) return null;
5310
5332
  const xml = readFileSync6(plistPath(), "utf8");
@@ -5314,7 +5336,7 @@ function installedCommand() {
5314
5336
  if (process.platform === "linux") {
5315
5337
  const line = readCrontab().split("\n").find((l) => l.includes(CRON_MARKER));
5316
5338
  const m = line?.match(/-lc '([^']*)'/);
5317
- return m ? m[1] : null;
5339
+ return m ? m[1].replace(/\\%/g, "%") : null;
5318
5340
  }
5319
5341
  return null;
5320
5342
  }
@@ -5330,7 +5352,7 @@ async function installSchedule(slug, flags) {
5330
5352
  installJob(command);
5331
5353
  process.stderr.write(
5332
5354
  `
5333
- ${chalk.hex(ACCENT)("\u2713")} ${chalk.white("monthly refresh scheduled")} ${chalk.dim("(1st of each month, 10:00)")}
5355
+ ${chalk.hex(ACCENT)("\u2713")} ${chalk.white("monthly refresh scheduled")} ${chalk.dim("(first awake hour of each month)")}
5334
5356
  ` + chalk.dim(` runs: ${command}
5335
5357
  `) + chalk.dim(` log: ${schedLogPath()}
5336
5358
  `) + chalk.dim(" disable anytime with `npx standout schedule off`\n\n")
@@ -5377,19 +5399,21 @@ async function scheduleStatus() {
5377
5399
  );
5378
5400
  return;
5379
5401
  }
5380
- const command = installedCommand();
5381
- if (!command) {
5402
+ const shell = installedShellCommand();
5403
+ if (!shell) {
5382
5404
  process.stderr.write(
5383
5405
  `
5384
5406
  ${chalk.white("no monthly schedule installed")}` + (isOptedOut() ? chalk.dim(" (opted out)") : "") + "\n" + chalk.dim(" enable with `npx standout schedule`\n\n")
5385
5407
  );
5386
5408
  return;
5387
5409
  }
5410
+ const stamp = readRefreshStamp();
5388
5411
  process.stderr.write(
5389
5412
  `
5390
- ${chalk.hex(ACCENT)("\u2713")} ${chalk.white("monthly refresh is on")} ${chalk.dim("(1st of each month, 10:00)")}
5391
- ` + chalk.dim(` runs: ${command}
5413
+ ${chalk.hex(ACCENT)("\u2713")} ${chalk.white("monthly refresh is on")} ${chalk.dim("(first awake hour of each month)")}
5414
+ ` + chalk.dim(` runs: ${extractRunCommand(shell) ?? shell}
5392
5415
  `) + chalk.dim(` log: ${schedLogPath()}
5416
+ `) + chalk.dim(` last refreshed month: ${stamp ?? "never"}
5393
5417
  `)
5394
5418
  );
5395
5419
  if (process.platform === "darwin") {
@@ -5418,24 +5442,28 @@ async function scheduleStatus() {
5418
5442
  }
5419
5443
  process.stderr.write("\n");
5420
5444
  }
5445
+ function autoRefreshNotice(headline) {
5446
+ const optOut = "npx standout schedule off";
5447
+ return `
5448
+ ${chalk.hex(ACCENT)("\u27F3")} ${chalk.bold.white(headline)}
5449
+ ${chalk.white("standout re-runs once a month so your wrapped keeps building")}
5450
+ ${chalk.white("(because local logs only keep ~30 days of history).")}
5451
+ ${chalk.white("opt out anytime:")} ${chalk.hex(ACCENT)(optOut)}
5452
+
5453
+ `;
5454
+ }
5421
5455
  async function maybeAutoInstallSchedule(slug) {
5422
5456
  try {
5457
+ if (process.env.STANDOUT_SCHEDULED === "1") return;
5423
5458
  if (!supportedPlatform() || isOptedOut()) return;
5424
5459
  const command = buildRunCommand(slug, []);
5425
- if (installedCommand() === command) {
5426
- process.stderr.write(
5427
- chalk.dim(
5428
- " monthly refresh is on \u2014 `npx standout schedule off` to disable\n\n"
5429
- )
5430
- );
5460
+ if (installedShellCommand() === buildGuardedCommand(command)) {
5461
+ writeRefreshStamp();
5462
+ process.stderr.write(autoRefreshNotice("auto-refresh is on"));
5431
5463
  return;
5432
5464
  }
5433
5465
  installJob(command);
5434
- process.stderr.write(
5435
- ` ${chalk.hex(ACCENT)("\u2713")} ${chalk.white("monthly refresh scheduled")} ${chalk.dim("(1st of each month) \u2014 disable with `npx standout schedule off`")}
5436
-
5437
- `
5438
- );
5466
+ process.stderr.write(autoRefreshNotice("auto-refresh scheduled"));
5439
5467
  } catch (error) {
5440
5468
  process.stderr.write(
5441
5469
  chalk.dim(
@@ -7703,6 +7731,7 @@ async function runAgent(jobId) {
7703
7731
  `
7704
7732
  );
7705
7733
  }
7734
+ await maybeAutoInstallSchedule(jobId);
7706
7735
  await maybeShareWrapped(wrapped);
7707
7736
  } else {
7708
7737
  process.stderr.write(
@@ -7872,7 +7901,6 @@ async function main() {
7872
7901
  } else {
7873
7902
  if (jobId) process.env.STANDOUT_JOB_ID = jobId;
7874
7903
  await runAgent(jobId);
7875
- await maybeAutoInstallSchedule(jobId);
7876
7904
  }
7877
7905
  }
7878
7906
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "standout",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Build your developer profile with AI. One command, zero friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",