zam-core 0.4.0 → 0.4.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.
@@ -326,7 +326,7 @@ The `capture-ui` command supports:
326
326
 
327
327
  On Windows, uses PowerShell/.NET for screen capture. On macOS, uses `screencapture`.
328
328
 
329
- **Observer permissions (Layer 2, ADR-0001).** `capture-ui` enforces a
329
+ **Observer permissions (Layer 2).** `capture-ui` enforces a
330
330
  user-configurable policy resolved from `zam settings`: `observer.scope`
331
331
  (`off` | `window` | `fullscreen`), `observer.allowlist`, `observer.denylist`,
332
332
  `observer.consent`, `observer.retention`. Set them with e.g.
package/README.de.md CHANGED
@@ -63,6 +63,25 @@ Einstieg: `zam whoami --set <deine-id>`
63
63
 
64
64
  ---
65
65
 
66
+ ## 🔄 ZAM aktuell halten
67
+
68
+ Prüfe, ob eine neuere Version vorliegt, und aktualisiere — ZAM wählt den passenden Mechanismus je nachdem, wie diese Kopie installiert wurde:
69
+
70
+ ```bash
71
+ zam update # neueste Version anwenden (fragt nach; -y überspringt)
72
+ zam update check # nur prüfen, ob ein Update verfügbar ist
73
+ ```
74
+
75
+ Was `zam update` je nach Installationskanal tut:
76
+
77
+ - **Developer** (Quell-Checkout, Standard für Mitwirkende) — holt den neuesten Quellcode, installiert Abhängigkeiten neu, baut die CLI und frischt die Skill-Dateien auf (`zam setup --force`). Danach den Agent-Client (z. B. Claude Code) neu starten, damit der aktualisierte `/zam`-Skill geladen wird.
78
+ - **winget / Homebrew** — delegiert an `winget upgrade` / `brew upgrade`, damit eine paketverwaltete Installation nie selbst ersetzt wird.
79
+ - **Direkt-Download / Desktop** — installiert ein signiertes In-place-Update über ZAM Desktop.
80
+
81
+ `zam update` verweigert einen Developer-Checkout mit uncommitteten Änderungen; committe oder stashe sie zuerst, oder nutze `--force`. Design-Details: [ADR „Approachable Setup and Self-Update“](docs/adr/2026-06-13b-approachable-setup-and-self-update.md).
82
+
83
+ ---
84
+
66
85
  ## 🏛 Vision: Eine paradiesische Zukunft
67
86
 
68
87
  ZAM ist das Werkzeug für den Übergang in eine Welt, in der Fürsorge und gemeinsames Wachstum die Währung sind.
package/README.md CHANGED
@@ -83,6 +83,25 @@ Token deletion is global. Card deletion is per-user.
83
83
 
84
84
  ---
85
85
 
86
+ ## 🔄 Keeping ZAM up to date
87
+
88
+ Check whether a newer release is out, then update — ZAM picks the right mechanism for how this copy was installed:
89
+
90
+ ```bash
91
+ zam update # apply the latest release (asks first; -y to skip)
92
+ zam update check # only report whether an update is available
93
+ ```
94
+
95
+ What `zam update` does per install channel:
96
+
97
+ - **Developer** (source checkout, the default for contributors) — pulls the latest source, reinstalls dependencies, rebuilds the CLI, and refreshes the skill files (`zam setup --force`) in the current instance. Restart your agent client (e.g. Claude Code) afterwards to load the refreshed `/zam` skill.
98
+ - **winget / Homebrew** — defers to `winget upgrade` / `brew upgrade`, so a package-managed install is never self-replaced.
99
+ - **Direct download / desktop** — applies a signed in-place update through ZAM Desktop.
100
+
101
+ `zam update` refuses to touch a developer checkout with uncommitted changes; commit or stash them first, or pass `--force`. See the [“Approachable Setup and Self-Update” ADR](docs/adr/2026-06-13b-approachable-setup-and-self-update.md) for the design.
102
+
103
+ ---
104
+
86
105
  ## 🏛 Vision: A Flourishing Future
87
106
 
88
107
  ZAM is a tool for the transition to a world where care and shared growth are the common currency.
package/dist/cli/index.js CHANGED
@@ -4847,6 +4847,37 @@ function decideUpdate(input8) {
4847
4847
  };
4848
4848
  }
4849
4849
  }
4850
+ function planUpdate(decision) {
4851
+ if (!decision.updateAvailable) return [];
4852
+ switch (decision.channel) {
4853
+ case "developer":
4854
+ return [
4855
+ { kind: "git-pull", label: "Pull the latest source" },
4856
+ { kind: "npm-install", label: "Install dependencies" },
4857
+ { kind: "npm-build", label: "Rebuild the CLI" },
4858
+ {
4859
+ kind: "distribute-skills",
4860
+ label: "Refresh skill files (zam setup --force)"
4861
+ }
4862
+ ];
4863
+ case "winget":
4864
+ case "homebrew":
4865
+ return [
4866
+ {
4867
+ kind: "run-command",
4868
+ label: `Update via ${decision.channel}`,
4869
+ command: decision.command
4870
+ }
4871
+ ];
4872
+ case "direct":
4873
+ return [
4874
+ {
4875
+ kind: "self-update",
4876
+ label: "Apply the signed update via the desktop app"
4877
+ }
4878
+ ];
4879
+ }
4880
+ }
4850
4881
 
4851
4882
  // src/cli/commands/agent.ts
4852
4883
  var C = {
@@ -5267,8 +5298,8 @@ async function startLocalRunner(url, model, locale) {
5267
5298
  process.stdout.write("\n");
5268
5299
  console.log(`\x1B[33m${t(locale, "wait_warning")}\x1B[0m`);
5269
5300
  console.log(`\x1B[2m${t(locale, "wait_info")}\x1B[0m`);
5270
- const { confirm: confirm4 } = await import("@inquirer/prompts");
5271
- const keepWaiting = await confirm4({
5301
+ const { confirm: confirm5 } = await import("@inquirer/prompts");
5302
+ const keepWaiting = await confirm5({
5272
5303
  message: t(locale, "keep_waiting_llm"),
5273
5304
  default: true
5274
5305
  }).catch(() => false);
@@ -5412,8 +5443,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
5412
5443
  }
5413
5444
  console.log(`
5414
5445
  \x1B[33m${t(locale, "local_ai_working")}\x1B[0m`);
5415
- const { confirm: confirm4 } = await import("@inquirer/prompts");
5416
- const keepWaiting = await confirm4({
5446
+ const { confirm: confirm5 } = await import("@inquirer/prompts");
5447
+ const keepWaiting = await confirm5({
5417
5448
  message: t(locale, "keep_waiting"),
5418
5449
  default: true
5419
5450
  }).catch(() => false);
@@ -10603,9 +10634,11 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
10603
10634
  });
10604
10635
 
10605
10636
  // src/cli/commands/update.ts
10606
- import { readFileSync as readFileSync13 } from "fs";
10637
+ import { spawnSync as spawnSync2 } from "child_process";
10638
+ import { existsSync as existsSync20, readFileSync as readFileSync13, realpathSync } from "fs";
10607
10639
  import { dirname as dirname9, join as join20 } from "path";
10608
10640
  import { fileURLToPath as fileURLToPath4 } from "url";
10641
+ import { confirm as confirm3 } from "@inquirer/prompts";
10609
10642
  import { Command as Command21 } from "commander";
10610
10643
  var GITHUB_REPO = "zam-os/zam";
10611
10644
  var CHANNELS = [
@@ -10620,6 +10653,7 @@ var C4 = {
10620
10653
  cyan: "\x1B[36m",
10621
10654
  dim: "\x1B[2m",
10622
10655
  green: "\x1B[32m",
10656
+ red: "\x1B[31m",
10623
10657
  yellow: "\x1B[33m"
10624
10658
  };
10625
10659
  function currentVersion() {
@@ -10635,6 +10669,16 @@ function currentVersion() {
10635
10669
  }
10636
10670
  return "0.0.0";
10637
10671
  }
10672
+ function versionAt(dir) {
10673
+ try {
10674
+ const pkg2 = JSON.parse(
10675
+ readFileSync13(join20(dir, "package.json"), "utf-8")
10676
+ );
10677
+ return pkg2.version ?? "unknown";
10678
+ } catch {
10679
+ return "unknown";
10680
+ }
10681
+ }
10638
10682
  async function fetchLatestVersion(repo) {
10639
10683
  const res = await fetch(
10640
10684
  `https://api.github.com/repos/${repo}/releases/latest`,
@@ -10704,7 +10748,150 @@ var checkCmd = new Command21("check").description("Check whether a newer ZAM has
10704
10748
  }
10705
10749
  }
10706
10750
  );
10707
- var updateCommand = new Command21("update").description("Check for ZAM updates").addCommand(checkCmd);
10751
+ function findSourceRepo() {
10752
+ let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
10753
+ let parent = dirname9(dir);
10754
+ while (parent !== dir) {
10755
+ if (existsSync20(join20(dir, ".git"))) return dir;
10756
+ dir = parent;
10757
+ parent = dirname9(dir);
10758
+ }
10759
+ return existsSync20(join20(dir, ".git")) ? dir : null;
10760
+ }
10761
+ function runGit(cwd, args, capture) {
10762
+ const res = spawnSync2("git", args, {
10763
+ cwd,
10764
+ stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
10765
+ encoding: "utf8"
10766
+ });
10767
+ return { ok: res.status === 0, out: (res.stdout ?? "").trim() };
10768
+ }
10769
+ function runNpm2(args, cwd) {
10770
+ const res = spawnSync2("npm", args, {
10771
+ cwd,
10772
+ stdio: "inherit",
10773
+ shell: process.platform === "win32"
10774
+ });
10775
+ return res.status ?? 1;
10776
+ }
10777
+ function runShell(command) {
10778
+ const res = spawnSync2(command, { stdio: "inherit", shell: true });
10779
+ return res.status === 0;
10780
+ }
10781
+ function applyDeveloperUpdate(force) {
10782
+ if (!hasCommand("git")) {
10783
+ console.error(`${C4.red}\u2717${C4.reset} git was not found on PATH.`);
10784
+ process.exit(1);
10785
+ }
10786
+ const src = findSourceRepo();
10787
+ if (!src) {
10788
+ console.error(
10789
+ `${C4.red}\u2717${C4.reset} Could not locate the ZAM source checkout to update.`
10790
+ );
10791
+ process.exit(1);
10792
+ }
10793
+ const status = runGit(src, ["status", "--porcelain"], true);
10794
+ if (status.ok && status.out && !force) {
10795
+ console.error(
10796
+ `${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
10797
+ ${status.out}
10798
+ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
10799
+ );
10800
+ process.exit(1);
10801
+ }
10802
+ console.log(
10803
+ `${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
10804
+ );
10805
+ if (!runGit(src, ["pull", "--ff-only"], false).ok) {
10806
+ console.error(
10807
+ `${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
10808
+ );
10809
+ process.exit(1);
10810
+ }
10811
+ console.log(`${C4.dim}\u2192 npm install${C4.reset}`);
10812
+ if (runNpm2(["install"], src) !== 0) {
10813
+ console.error(`${C4.red}\u2717${C4.reset} npm install failed.`);
10814
+ process.exit(1);
10815
+ }
10816
+ console.log(`${C4.dim}\u2192 npm run build${C4.reset}`);
10817
+ if (runNpm2(["run", "build"], src) !== 0) {
10818
+ console.error(`${C4.red}\u2717${C4.reset} Build failed.`);
10819
+ process.exit(1);
10820
+ }
10821
+ console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
10822
+ const setup = spawnSync2(
10823
+ process.execPath,
10824
+ [join20(src, "dist", "cli", "index.js"), "setup", "--force"],
10825
+ { cwd: process.cwd(), stdio: "inherit" }
10826
+ );
10827
+ if (setup.status !== 0) {
10828
+ console.warn(
10829
+ `${C4.yellow}\u26A0${C4.reset} Skill refresh reported a problem \u2014 run '${C4.cyan}zam setup --force${C4.reset}' here manually.`
10830
+ );
10831
+ }
10832
+ console.log(
10833
+ `
10834
+ ${C4.green}\u2713${C4.reset} Updated to ${C4.cyan}${versionAt(src)}${C4.reset}. Restart your agent client (e.g. Claude Code) to load the refreshed ${C4.cyan}/zam${C4.reset} skill.`
10835
+ );
10836
+ }
10837
+ async function applyUpdate(opts) {
10838
+ try {
10839
+ const current = currentVersion();
10840
+ const latest = await fetchLatestVersion(GITHUB_REPO);
10841
+ const channel = getInstallChannel();
10842
+ const decision = decideUpdate({
10843
+ currentVersion: current,
10844
+ latestVersion: latest,
10845
+ channel
10846
+ });
10847
+ if (!decision.updateAvailable) {
10848
+ console.log(
10849
+ `${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${current}${C4.reset}).`
10850
+ );
10851
+ return;
10852
+ }
10853
+ console.log(
10854
+ `${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${current}${C4.reset} \u2192 ${C4.cyan}${latest}${C4.reset} ${C4.dim}(${channel} install)${C4.reset}`
10855
+ );
10856
+ for (const step of planUpdate(decision)) {
10857
+ console.log(` ${C4.dim}\u2022${C4.reset} ${step.label}`);
10858
+ }
10859
+ if (channel === "direct") {
10860
+ console.log(
10861
+ `
10862
+ ${C4.dim}This install updates through the desktop app's signed updater \u2014 open ZAM Desktop to install ${latest}.${C4.reset}`
10863
+ );
10864
+ return;
10865
+ }
10866
+ if (!opts.yes) {
10867
+ const ok = await confirm3({
10868
+ message: "Apply this update?",
10869
+ default: true
10870
+ });
10871
+ if (!ok) {
10872
+ console.log("Aborted.");
10873
+ return;
10874
+ }
10875
+ }
10876
+ console.log();
10877
+ if (channel === "winget" || channel === "homebrew") {
10878
+ if (!decision.command || !runShell(decision.command)) process.exit(1);
10879
+ return;
10880
+ }
10881
+ applyDeveloperUpdate(opts.force ?? false);
10882
+ } catch (err) {
10883
+ console.error("Error:", err.message);
10884
+ process.exit(1);
10885
+ }
10886
+ }
10887
+ var updateCommand = new Command21("update").description(
10888
+ "Update ZAM to the latest release (use `update check` to only check)"
10889
+ ).option("-y, --yes", "Apply without confirmation").option(
10890
+ "--force",
10891
+ "Update even if the source checkout has uncommitted changes"
10892
+ ).action(async (opts) => {
10893
+ await applyUpdate(opts);
10894
+ }).addCommand(checkCmd);
10708
10895
 
10709
10896
  // src/cli/commands/whoami.ts
10710
10897
  import { Command as Command22 } from "commander";
@@ -10745,11 +10932,11 @@ var whoamiCommand = new Command22("whoami").description("Show or set the default
10745
10932
 
10746
10933
  // src/cli/commands/workspace.ts
10747
10934
  import { execSync as execSync6 } from "child_process";
10748
- import { existsSync as existsSync20, writeFileSync as writeFileSync11 } from "fs";
10935
+ import { existsSync as existsSync21, writeFileSync as writeFileSync11 } from "fs";
10749
10936
  import { join as join21 } from "path";
10750
- import { confirm as confirm3, input as input7 } from "@inquirer/prompts";
10937
+ import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
10751
10938
  import { Command as Command23 } from "commander";
10752
- function runGit(cwd, args) {
10939
+ function runGit2(cwd, args) {
10753
10940
  try {
10754
10941
  return execSync6(`git ${args}`, {
10755
10942
  cwd,
@@ -10779,7 +10966,7 @@ workspaceCommand.command("publish").description("Publish your local workspace sa
10779
10966
  );
10780
10967
  process.exit(1);
10781
10968
  }
10782
- if (!existsSync20(workspaceDir)) {
10969
+ if (!existsSync21(workspaceDir)) {
10783
10970
  console.error(
10784
10971
  `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
10785
10972
  );
@@ -10794,19 +10981,19 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
10794
10981
  process.exit(1);
10795
10982
  }
10796
10983
  const gitignorePath = join21(workspaceDir, ".gitignore");
10797
- if (!existsSync20(gitignorePath)) {
10984
+ if (!existsSync21(gitignorePath)) {
10798
10985
  writeFileSync11(
10799
10986
  gitignorePath,
10800
10987
  "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
10801
10988
  "utf8"
10802
10989
  );
10803
10990
  }
10804
- const hasGitRepo = existsSync20(join21(workspaceDir, ".git"));
10991
+ const hasGitRepo = existsSync21(join21(workspaceDir, ".git"));
10805
10992
  if (!hasGitRepo) {
10806
10993
  console.log("Initializing local Git repository...");
10807
- runGit(workspaceDir, "init -b main");
10808
- runGit(workspaceDir, "add .");
10809
- runGit(
10994
+ runGit2(workspaceDir, "init -b main");
10995
+ runGit2(workspaceDir, "add .");
10996
+ runGit2(
10810
10997
  workspaceDir,
10811
10998
  'commit -m "chore: initial workspace sandbox bootstrap"'
10812
10999
  );
@@ -10818,14 +11005,14 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
10818
11005
  message: "Choose a name for your GitHub repository:",
10819
11006
  default: "zam-personal"
10820
11007
  });
10821
- const isPrivate = await confirm3({
11008
+ const isPrivate = await confirm4({
10822
11009
  message: "Should the repository be private?",
10823
11010
  default: true
10824
11011
  });
10825
11012
  const repoVisibility = isPrivate ? "--private" : "--public";
10826
11013
  if (hasCommand("gh")) {
10827
11014
  console.log("GitHub CLI detected! Automating repository creation...");
10828
- const proceedGh = await confirm3({
11015
+ const proceedGh = await confirm4({
10829
11016
  message: "Would you like ZAM to create the repository using the GitHub CLI?",
10830
11017
  default: true
10831
11018
  });
@@ -10870,16 +11057,16 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
10870
11057
  console.log("Linking remote repository and pushing...");
10871
11058
  let hasOrigin = false;
10872
11059
  try {
10873
- runGit(workspaceDir, "remote get-url origin");
11060
+ runGit2(workspaceDir, "remote get-url origin");
10874
11061
  hasOrigin = true;
10875
11062
  } catch {
10876
11063
  }
10877
11064
  if (hasOrigin) {
10878
- runGit(workspaceDir, `remote set-url origin ${githubUrl}`);
11065
+ runGit2(workspaceDir, `remote set-url origin ${githubUrl}`);
10879
11066
  } else {
10880
- runGit(workspaceDir, `remote add origin ${githubUrl}`);
11067
+ runGit2(workspaceDir, `remote add origin ${githubUrl}`);
10881
11068
  }
10882
- runGit(workspaceDir, "push -u origin main");
11069
+ runGit2(workspaceDir, "push -u origin main");
10883
11070
  console.log(
10884
11071
  "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
10885
11072
  );