@astrofoundry/pi-astro 0.18.1 → 0.18.2

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/agents/arcane.md CHANGED
@@ -13,7 +13,7 @@ You are the Arcane specialist. The `arcane` tool is your only way to reach Arcan
13
13
  Rules:
14
14
 
15
15
  - Read first. Run `arcane` with `["projects", "list"]` or `["containers", "list"]` before proposing a change.
16
- - Project definitions (Compose files, `.env` layout, project folders) change only through `gitops write`, `gitops commit`, `gitops push`, then a sync or `projects up`. Never try to edit a running project through the CLI; the tool refuses it.
16
+ - Project definitions of GitOps-managed projects (Compose files, `.env` layout, project folders) change only through `gitops show`, `gitops write`, `gitops commit`, `gitops push`, then a sync or `projects up`. Never edit a managed project through `projects update` or `projects workspace`, even though the CLI allows it.
17
17
  - Operational actions (restart, logs, stats, redeploy, image pull, prune) go straight through the CLI.
18
18
  - Confirm destructive operations (`down`, `delete`, `prune`) are explicitly requested in the task before running them.
19
19
  - Report facts from the JSON output. Sensitive fields are already removed; never guess at what was removed.
@@ -247,10 +247,18 @@ export default function astroSubagents(pi: ExtensionAPI, options: GateOptions =
247
247
  }
248
248
  const agents = listAgents(ctx.cwd, "user").agents;
249
249
  ctx.ui.notify(`running ${parsed.agent}...`, "info");
250
- const result = await runOne(ctx, agents, parsed.agent, parsed.task, undefined, undefined, undefined, undefined);
250
+ ctx.ui.setStatus("subagent", `${parsed.agent} running`);
251
+ let result: RunResult;
252
+ try {
253
+ result = await runOne(ctx, agents, parsed.agent, parsed.task, undefined, undefined, undefined, undefined);
254
+ } finally {
255
+ ctx.ui.setStatus("subagent", undefined);
256
+ }
257
+ const status = isFailed(result) ? "failed" : "done";
258
+ ctx.ui.notify(`${parsed.agent} ${status}`, isFailed(result) ? "warning" : "info");
251
259
  pi.sendMessage(
252
- { customType: "astro-subagents", content: `[${parsed.agent}] ${isFailed(result) ? "failed" : "done"}\n\n${resultOutput(result)}`, display: true },
253
- { deliverAs: "nextTurn" },
260
+ { customType: "astro-subagents", content: `Result from /run ${parsed.agent} (${status}). Task: ${parsed.task}\n\n${resultOutput(result)}`, display: true },
261
+ { deliverAs: "followUp", triggerTurn: true },
254
262
  );
255
263
  },
256
264
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrofoundry/pi-astro",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "Personal pi customizations (extensions, subagents, skills, prompts, themes) for the pi coding agent.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -21,11 +21,13 @@ Arcane manages the Docker host on VM 100 (`10.0.40.30`). Every app is a Compose
21
21
 
22
22
  ## Two kinds of operations
23
23
 
24
- 1. **Definitions**: Compose files, env layout, new or removed projects. Only through the repository:
25
- `gitops pull` → `gitops write <path> <content>` → `gitops diff` → `gitops commit <message>` → `gitops push` → then `["projects", "up", "<name>"]` or a GitOps sync through the CLI, then verify with `["projects", "list"]` and container logs.
26
- 2. **Operations**: restart, logs, stats, redeploy, pull images, prune. Straight through the CLI.
24
+ 1. **Definitions** of GitOps-managed projects (every project whose `gitOpsManagedBy` field is set, including `arcane` itself): Compose files, env layout, new or removed projects. Only through the repository:
25
+ `gitops pull` → `gitops ls` / `gitops show <path>` to read the current files → `gitops write <path> <content>` → `gitops diff` → `gitops commit <message>` → `gitops push` → then the project's GitOps sync or `["projects", "up", "<name>"]`, then verify with `["projects", "list"]` and container logs. Editing a managed project directly (`projects update`, `projects workspace`) makes the repository lie; do not do it.
26
+ 2. **Operations**: restart, logs, stats, redeploy, pull images, upgrade, prune. Straight through the CLI.
27
27
 
28
- The tool refuses `projects create|edit|update|import`, `config`, `auth`, `self-update`, `completion`.
28
+ The full `arcane-cli` is available. The tool refuses only `config`, `auth`, `self-update`, `completion`, which would change its own setup.
29
+
30
+ Updating Arcane itself: the `arcane` project is GitOps-managed. Bump the image tag in its Compose file through gitops, push, sync. The sync redeploys the manager that runs the sync; if the manager does not come back, tell the caller to finish with `docker compose pull && docker compose up -d` in `/opt/docker/arcane` on VM 100.
29
31
 
30
32
  ## Output
31
33
 
@@ -38,7 +38,7 @@ Wrappers ship as TypeScript and run under Node 24 type stripping: erasable synta
38
38
  - Exit codes: `UsageError` 2, `ServiceError` 1, success 0. Diagnostics on stderr, results on stdout, JSON where possible.
39
39
  - Output filters: Arcane responses pass `stripKeys(ARCANE_DENIED_KEYS)`; Zitadel responses pass `redactSecrets`. Add keys there rather than in a wrapper.
40
40
  - Fixed remote commands: `DMZ_COMMANDS` and `PULSAR_COMMANDS` must match the entry scripts in `entry/remote/` and their tracked copies in the homelab repository (`02-pulsar-proxmox/dmz/system/`, `02-pulsar-proxmox/pulsar/system/`). Change both sides in the same commit and redeploy the remote script.
41
- - Arcane definitions change only through `gitops`; `REFUSED_PREFIXES` keeps the CLI from bypassing it and from touching its own config or auth.
41
+ - `REFUSED_PREFIXES` protects only the wrapper itself (`config`, `auth`, `self-update`, `completion`). The GitOps-first rule for managed projects lives in the skill and the agent prompt, not in code, because unmanaged projects and emergencies need the direct CLI.
42
42
  - Every wrapper answers `--help` without reading config, so agents can discover subcommands before anything else is set up.
43
43
 
44
44
  ## Gate
@@ -4,7 +4,7 @@ A specialist is a Pi subagent that is the only way to operate one area of the ho
4
4
 
5
5
  | Agent | Area | What it can do |
6
6
  |---|---|---|
7
- | `astro.arcane` | Arcane container platform (VM 100) and its GitOps repository | Every `arcane-cli` operation; Compose changes through `gitops write|commit|push`, then sync |
7
+ | `astro.arcane` | Arcane container platform (VM 100) and its GitOps repository | Every `arcane-cli` operation except its own `config`, `auth`, `self-update`; Compose changes of GitOps-managed projects through `gitops show|write|commit|push`, then sync |
8
8
  | `astro.identity` | Zitadel; Pomerium, nginx, lego on VM 104 | Any Zitadel API call (v2, management, admin, auth) with responses redacted; `dmz status|journal|config-template|render|pomerium-restart|nginx-reload|lego-renew` |
9
9
  | `astro.network` | UniFi, FreeRADIUS (LXC 108), Tailscale (LXC 105) | Read UniFi sites, devices, clients, networks, firewall policies, WANs; RADIUS and Tailscale checks on Pulsar |
10
10
 
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, relative, resolve } from "node:path";
3
3
  import { readConfig } from "../lib/config.ts";
4
4
  import { ServiceError, UsageError } from "../lib/errors.ts";
@@ -32,17 +32,8 @@ const SHAPE = {
32
32
  gitopsBranch: "string",
33
33
  } as const;
34
34
 
35
- /** Definitions change through GitOps; these CLI paths would bypass it or alter the wrapper's own setup. */
36
- export const REFUSED_PREFIXES: readonly string[] = [
37
- "projects create",
38
- "projects edit",
39
- "projects update",
40
- "projects import",
41
- "config",
42
- "auth",
43
- "self-update",
44
- "completion",
45
- ];
35
+ /** CLI paths that would alter the wrapper's own setup or the binary; everything else passes through. */
36
+ export const REFUSED_PREFIXES: readonly string[] = ["config", "auth", "self-update", "completion"];
46
37
 
47
38
  /** Global arcane-cli flags that take a separate value. */
48
39
  const VALUE_FLAGS = ["--env", "--output", "--config", "-c", "--request-timeout", "--log-level", "--limit"];
@@ -75,13 +66,15 @@ const HELP = `arcane specialist
75
66
  <arcane-cli args...> run arcane-cli with a fresh federated token; JSON output, sensitive fields removed
76
67
  gitops status branch, ahead/behind, changed files of the GitOps checkout
77
68
  gitops pull fast-forward the checkout from origin
69
+ gitops ls [dir] list files in the checkout
70
+ gitops show <path> print a file from the checkout
78
71
  gitops diff unstaged and staged changes
79
72
  gitops log [n] last n commits (default 10)
80
73
  gitops write <path> <content> write a file inside the checkout (creates folders)
81
74
  gitops commit <message> stage everything and commit
82
75
  gitops push push the branch to origin
83
76
 
84
- Refused: ${REFUSED_PREFIXES.join(", ")}. Project definitions change through gitops, then sync with the CLI.`;
77
+ Refused: ${REFUSED_PREFIXES.join(", ")}. GitOps-managed projects (gitOpsManagedBy set) change through gitops, then sync.`;
85
78
 
86
79
  const TIMEOUT_MS = 240_000;
87
80
 
@@ -147,6 +140,20 @@ async function gitops(config: ArcaneConfig, args: string[]): Promise<number> {
147
140
  case "diff":
148
141
  printRaw((await git(dir, ["diff", "HEAD"])) || "(no changes)");
149
142
  return 0;
143
+ case "ls": {
144
+ if (rest.length > 1) throw new UsageError("gitops ls [dir]");
145
+ const target = rest[0] === undefined ? dir : safeRepoPath(dir, rest[0]);
146
+ if (!existsSync(target) || !statSync(target).isDirectory()) throw new UsageError(`not a directory in the checkout: ${rest[0] ?? "."}`);
147
+ printJson(readdirSync(target, { withFileTypes: true }).filter((e) => e.name !== ".git").map((e) => (e.isDirectory() ? `${e.name}/` : e.name)).sort());
148
+ return 0;
149
+ }
150
+ case "show": {
151
+ if (rest.length !== 1) throw new UsageError("gitops show <path>");
152
+ const target = safeRepoPath(dir, rest[0]);
153
+ if (!existsSync(target) || !statSync(target).isFile()) throw new UsageError(`not a file in the checkout: ${rest[0]}`);
154
+ printRaw(readFileSync(target, "utf-8"));
155
+ return 0;
156
+ }
150
157
  case "log": {
151
158
  const n = rest[0] === undefined ? 10 : Number(rest[0]);
152
159
  if (!Number.isInteger(n) || n <= 0 || n > 200) throw new UsageError("gitops log [n]: n must be 1..200");
@@ -211,7 +218,7 @@ async function federatedToken(config: ArcaneConfig): Promise<string> {
211
218
 
212
219
  async function passthrough(config: ArcaneConfig, args: string[]): Promise<number> {
213
220
  const refused = refusedPrefix(args);
214
- if (refused) throw new UsageError(`"${refused}" is refused; project definitions change through gitops`);
221
+ if (refused) throw new UsageError(`"${refused}" is refused by the wrapper`);
215
222
  const token = await federatedToken(config);
216
223
  const wantsJson = args.includes("--json") || args.includes("--output");
217
224
  const cliArgs = ["--config", config.arcaneCliConfig, ...(wantsJson ? [] : ["--output", "json"]), "--no-color", ...args];
@@ -8,9 +8,11 @@ import { UsageError } from "./lib/errors.ts";
8
8
  import { buildPulsarRemote, parseQuery, unifiPath, command as networkCommand } from "./network/run.ts";
9
9
 
10
10
  describe("arcane wrapper", () => {
11
- it("refuses CLI paths that bypass GitOps or touch the wrapper setup", () => {
12
- expect(refusedPrefix(["projects", "create", "x"])).toBe("projects create");
11
+ it("refuses only the wrapper's own setup paths", () => {
12
+ expect(refusedPrefix(["projects", "create", "x"])).toBeNull();
13
+ expect(refusedPrefix(["projects", "update", "arcane", "--flag"])).toBeNull();
13
14
  expect(refusedPrefix(["--env", "0", "auth", "login"])).toBe("auth");
15
+ expect(refusedPrefix(["self-update"])).toBe("self-update");
14
16
  expect(refusedPrefix(["config", "set", "api-key", "x"])).toBe("config");
15
17
  expect(refusedPrefix(["projects", "list"])).toBeNull();
16
18
  expect(refusedPrefix(["projects", "up", "my-app"])).toBeNull();
@@ -29,6 +31,7 @@ describe("arcane wrapper", () => {
29
31
  const write = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
30
32
  expect(await arcaneCommand(["--help"])).toBe(0);
31
33
  expect(String(write.mock.calls[0][0])).toContain("gitops write");
34
+ expect(String(write.mock.calls[0][0])).toContain("gitops show");
32
35
  write.mockRestore();
33
36
  });
34
37
  });