moshcode 0.38.0 → 0.40.0
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/README.md +66 -0
- package/bin/moshcode.mjs +3 -0
- package/package.json +1 -1
- package/prd/0010-cloud-settings-sync.md +132 -0
- package/prd/README.md +1 -0
- package/src/aliases.mjs +160 -0
- package/src/cli-schema.mjs +59 -0
- package/src/dns-system.mjs +176 -6
- package/src/dns.mjs +33 -1
- package/src/help.mjs +14 -1
- package/src/settings-sync.mjs +659 -0
- package/src/tui.mjs +161 -14
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ or miss one that does. A test fails the build when it drifts.
|
|
|
43
43
|
| `moshcode login` | account | authenticate with app.moshcode.sh |
|
|
44
44
|
| `moshcode whoami` | account | show the logged-in account |
|
|
45
45
|
| `moshcode logout` | account | clear the logged-in account |
|
|
46
|
+
| `moshcode save` | account | save this machine's pit settings to your account |
|
|
47
|
+
| `moshcode load` | account | bring your saved pit settings onto this machine |
|
|
46
48
|
| `moshcode console` | account | serve or connect to the browser terminal |
|
|
47
49
|
| `moshcode dns` | hosting | resolve Moshpit names on this machine |
|
|
48
50
|
| `moshcode doh` | hosting | run the DNS-over-HTTPS resolver |
|
|
@@ -543,6 +545,24 @@ an equity's score, and each response ships the `caveats` that say so. Prices are
|
|
|
543
545
|
Alpaca's US venue alone and can differ materially from other exchanges. Research
|
|
544
546
|
aid, not advice — and like `stocks`, nothing under `crypto` can place an order.
|
|
545
547
|
|
|
548
|
+
### Aliases (`/alias`)
|
|
549
|
+
|
|
550
|
+
The pit is a prompt you sit at all day, so it lets you name the lines you keep
|
|
551
|
+
retyping. An alias runs in `$SHELL` unless it starts with `/`, in which case it
|
|
552
|
+
is a pit command:
|
|
553
|
+
|
|
554
|
+
```text
|
|
555
|
+
/alias set gs "git status" # then /gs — and /gs -sb appends to it
|
|
556
|
+
/alias set cx "/agents codex" # a pit command, not a shell one
|
|
557
|
+
/alias # what is defined
|
|
558
|
+
/alias rm gs
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
They live in `~/.moshcode/aliases.json` (owner-only, like the history file) and
|
|
562
|
+
survive between sessions. A name that is already a pit command, an engine, or a
|
|
563
|
+
tool is refused rather than shadowed — built-ins are dispatched first, so such
|
|
564
|
+
an alias would never run.
|
|
565
|
+
|
|
546
566
|
### Social posting from the pit
|
|
547
567
|
|
|
548
568
|
The pit can hand a prepared post to Bluesky or Nostr without storing either
|
|
@@ -561,6 +581,52 @@ event, and publishes it to the displayed relays. Both flows leave the final
|
|
|
561
581
|
confirmation in the browser. If the pit is remote or headless, `/post` prints
|
|
562
582
|
the composer URL instead.
|
|
563
583
|
|
|
584
|
+
## Settings sync (`/save` and `/load`)
|
|
585
|
+
|
|
586
|
+
Your pit becomes yours by accretion — a dozen aliases, herd rules you tuned until
|
|
587
|
+
the roster stopped lying to you. All of it lives in `~/.moshcode` on one machine,
|
|
588
|
+
which is why every new laptop, container and droplet used to feel like someone
|
|
589
|
+
else's prompt.
|
|
590
|
+
|
|
591
|
+
`/save` pushes that configuration to your `app.moshcode.sh` account. `/load`
|
|
592
|
+
brings it down onto any machine you have run `/login` on.
|
|
593
|
+
|
|
594
|
+
```sh
|
|
595
|
+
moshcode save # push this machine's settings (pit: /save)
|
|
596
|
+
moshcode save --dry-run # what would go up, and stop
|
|
597
|
+
|
|
598
|
+
# on the new box
|
|
599
|
+
moshcode login
|
|
600
|
+
moshcode load # pull them down (pit: /load)
|
|
601
|
+
moshcode load --dry-run # the per-file plan, changing nothing
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
What syncs is an allowlist, not a directory walk:
|
|
605
|
+
|
|
606
|
+
| file | what it is |
|
|
607
|
+
|---|---|
|
|
608
|
+
| `~/.moshcode/aliases.json` | your pit aliases (`/alias`) |
|
|
609
|
+
| `~/.moshcode/herd/rules.json` | herd state-detection overrides |
|
|
610
|
+
|
|
611
|
+
What never syncs, by name: `credentials.json` (the account token this very
|
|
612
|
+
feature authenticates with), `herd/sessions.json` (live state pinned to one tmux
|
|
613
|
+
server), `sync.json`, and the `pkg/` binary cache. Engine configuration
|
|
614
|
+
(`~/.claude.json` and friends) is deliberately left alone — those files carry
|
|
615
|
+
provider API keys.
|
|
616
|
+
|
|
617
|
+
Nothing is overwritten quietly:
|
|
618
|
+
|
|
619
|
+
- Each save is a numbered **revision**. `/save` sends the revision it last agreed
|
|
620
|
+
on, and the app refuses the write if another machine has saved since — you get
|
|
621
|
+
told, with `/load` and `/save --force` as the two ways out.
|
|
622
|
+
- `/load` refuses to replace a settings file you edited since this machine last
|
|
623
|
+
synced, and names it. `--force` overrides.
|
|
624
|
+
- The last ten revisions are kept. See them, and which machine each came from, at
|
|
625
|
+
[app.moshcode.sh/settings/sync](https://app.moshcode.sh/settings/sync) — where
|
|
626
|
+
you can also promote an older revision or delete the lot.
|
|
627
|
+
|
|
628
|
+
Both verbs take `--json`, so a provisioning script can act on the result.
|
|
629
|
+
|
|
564
630
|
## Browser terminal (`moshcode console`)
|
|
565
631
|
|
|
566
632
|
A real terminal in the browser — arrow keys, history, full-screen TUIs — because
|
package/bin/moshcode.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { canOpenBrowser, openBrowser } from "../src/open-url.mjs";
|
|
|
26
26
|
import { locate, tilde } from "../src/pwd.mjs";
|
|
27
27
|
import { createPrd, listPrds, authoringPrompt } from "../src/prd.mjs";
|
|
28
28
|
import { loginAuto, whoami, logout } from "../src/auth.mjs";
|
|
29
|
+
import { loadCommand, saveCommand } from "../src/settings-sync.mjs";
|
|
29
30
|
import { tui } from "../src/tui.mjs";
|
|
30
31
|
import { consoleCommand } from "../src/console.mjs";
|
|
31
32
|
import { herdCommand, herdStart, splitDetachArgs } from "../src/herd-cli.mjs";
|
|
@@ -609,6 +610,8 @@ async function main() {
|
|
|
609
610
|
return;
|
|
610
611
|
}
|
|
611
612
|
if (cmd === "logout") { logout(); return; }
|
|
613
|
+
if (cmd === "save") { process.exitCode = await saveCommand(rest); return; }
|
|
614
|
+
if (cmd === "load") { process.exitCode = await loadCommand(rest); return; }
|
|
612
615
|
if (cmd === "run") {
|
|
613
616
|
let max = 3, dryRun = false;
|
|
614
617
|
let optionsEnded = false;
|
package/package.json
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
openprd: "0.2"
|
|
3
|
+
id: "0010"
|
|
4
|
+
title: "Sync the pit's settings to your moshcode.sh account"
|
|
5
|
+
status: Draft
|
|
6
|
+
authors:
|
|
7
|
+
- anthony@profullstack.com
|
|
8
|
+
created: 2026-08-11
|
|
9
|
+
updated: 2026-08-11
|
|
10
|
+
repo: https://github.com/moshcoder/moshcode
|
|
11
|
+
discussion:
|
|
12
|
+
implementation: src/settings-sync.mjs · apps/pwa/src/routes/settings-sync.mjs
|
|
13
|
+
tags: account, settings, sync
|
|
14
|
+
supersedes:
|
|
15
|
+
superseded-by:
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Problem
|
|
19
|
+
|
|
20
|
+
A pit becomes yours by accretion. You add `/alias set gs "git status"`, then a
|
|
21
|
+
dozen more; you tune the herd's rules until it stops calling a working agent
|
|
22
|
+
blocked. None of it is in a repo, none of it is in a dotfile anyone syncs, and
|
|
23
|
+
all of it lives in `~/.moshcode` on exactly one machine.
|
|
24
|
+
|
|
25
|
+
So the second machine is a stranger. A new laptop, a fresh container, a droplet
|
|
26
|
+
you SSH into to babysit an agent, a reinstall after a disk swap — each one starts
|
|
27
|
+
from nothing, and the muscle memory built at the first prompt does not work at
|
|
28
|
+
the second. People already log in to `app.moshcode.sh` (`/login`) for approvals,
|
|
29
|
+
notifications and the session mirror, so the account that could hold this
|
|
30
|
+
configuration is already there and already paired with every machine.
|
|
31
|
+
|
|
32
|
+
## Goals
|
|
33
|
+
|
|
34
|
+
- Moving to a new machine costs `/login` and `/load`, not an afternoon of
|
|
35
|
+
remembering what you had.
|
|
36
|
+
- A person can see what is stored on their account, and delete it, from the web.
|
|
37
|
+
- Nobody is ever surprised by a settings overwrite — not from another machine,
|
|
38
|
+
and not over their own uncommitted edits.
|
|
39
|
+
- No credential, key or token is ever part of what syncs, and that fact is
|
|
40
|
+
enforced by a test rather than by care.
|
|
41
|
+
|
|
42
|
+
## Non-Goals
|
|
43
|
+
|
|
44
|
+
- Continuous or background sync. Settings are edited by a person at a moment they
|
|
45
|
+
can name; a daemon that pushes silently is a daemon that overwrites silently.
|
|
46
|
+
- Syncing engine configuration (`~/.claude.json`, `~/.codex`, MCP registrations).
|
|
47
|
+
Those files carry provider API keys and are owned by other tools' schemas.
|
|
48
|
+
- Syncing machine state: live herd sessions, the package cache, shell history.
|
|
49
|
+
None of it means anything on a different box.
|
|
50
|
+
- Merging. Two divergent settings files are resolved by a person choosing one,
|
|
51
|
+
not by a three-way merge of someone's aliases.
|
|
52
|
+
|
|
53
|
+
## Users
|
|
54
|
+
|
|
55
|
+
- **The multi-machine moshcoder** — laptop, desktop, a dev box, and a container
|
|
56
|
+
per project. Wants the same prompt everywhere.
|
|
57
|
+
- **The reinstaller** — new OS, same person. Wants their aliases back.
|
|
58
|
+
- **The team lead** — one account, several machines, and a strong preference for
|
|
59
|
+
never explaining to a colleague why their aliases disappeared.
|
|
60
|
+
|
|
61
|
+
## Requirements
|
|
62
|
+
|
|
63
|
+
- R1 [P0] `/save` (and `moshcode save`) uploads this machine's pit settings to the
|
|
64
|
+
logged-in account. `/load` (`moshcode load`) brings them back down.
|
|
65
|
+
- R2 [P0] What syncs is an allowlist, not a directory walk: `aliases.json` and
|
|
66
|
+
`herd/rules.json` today. `credentials.json`, `herd/sessions.json`, `sync.json`
|
|
67
|
+
and `pkg/` are named as never-synced and asserted in tests.
|
|
68
|
+
- R3 [P0] Each save is a numbered revision. `/save` sends the revision it last
|
|
69
|
+
agreed on and the app refuses the write if the account has moved past it, so
|
|
70
|
+
two machines cannot silently erase one another.
|
|
71
|
+
- R4 [P0] `/load` refuses to overwrite a settings file that changed locally since
|
|
72
|
+
the last sync, and names the file. `--force` overrides; `--dry-run` shows the
|
|
73
|
+
per-file plan and writes nothing.
|
|
74
|
+
- R5 [P0] Every path in a downloaded snapshot is re-checked against the allowlist
|
|
75
|
+
before anything is written. A snapshot is data from the network, and an
|
|
76
|
+
unchecked path in it makes `/load` a remote write primitive.
|
|
77
|
+
- R6 [P1] The app keeps the last ten revisions, shows them at
|
|
78
|
+
`/settings/sync` with the machine and time each came from, and can promote an
|
|
79
|
+
older one to current.
|
|
80
|
+
- R7 [P1] `--json` on both verbs, so a provisioning script can act on the result.
|
|
81
|
+
- R8 [P1] A snapshot records which engines and tools the source machine had
|
|
82
|
+
installed. `/load` names the missing ones as a suggestion; it never installs.
|
|
83
|
+
- R9 [P2] Not logged in, session expired, nothing saved yet, conflict: each is a
|
|
84
|
+
sentence naming the command that resolves it (`/login`, `/save`, `/load`,
|
|
85
|
+
`--force`).
|
|
86
|
+
|
|
87
|
+
## UX Notes
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
mosh ▸ /save
|
|
91
|
+
✓ saved 2 files to you@example.com (revision 3)
|
|
92
|
+
aliases.json pit aliases
|
|
93
|
+
herd/rules.json herd state rules
|
|
94
|
+
on another machine: `/login` then `/load`
|
|
95
|
+
|
|
96
|
+
mosh ▸ /load # on the new box
|
|
97
|
+
loaded revision 3 from dev — 2 files written
|
|
98
|
+
added aliases.json
|
|
99
|
+
added herd/rules.json
|
|
100
|
+
that machine also had codex, gh — `/install <name>` to match it
|
|
101
|
+
|
|
102
|
+
mosh ▸ /load # after editing aliases locally
|
|
103
|
+
1 local file changed since this machine last synced:
|
|
104
|
+
aliases.json
|
|
105
|
+
`/save` to keep them, `/load --force` to replace them, `/load --dry-run` to see the difference
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The pit never blocks on this: both verbs are one request and some printing, so
|
|
109
|
+
readline keeps the prompt. `~/.moshcode/sync.json` remembers the revision and a
|
|
110
|
+
per-file digest — that digest is what separates "someone else saved" from "you
|
|
111
|
+
edited this five minutes ago", which want opposite answers.
|
|
112
|
+
|
|
113
|
+
## Success Metrics
|
|
114
|
+
|
|
115
|
+
- A fresh machine reaches a familiar prompt in two commands (`/login`, `/load`).
|
|
116
|
+
- Zero settings-loss reports: every destructive path is either refused or
|
|
117
|
+
recoverable from `/settings/sync`.
|
|
118
|
+
- No credential ever appears in a stored snapshot (asserted, not audited).
|
|
119
|
+
|
|
120
|
+
## Risks & Open Questions
|
|
121
|
+
|
|
122
|
+
- **Scope creep into secrets.** The most-requested next file will be an engine
|
|
123
|
+
config that holds an API key. Holding the line — settings, never credentials —
|
|
124
|
+
is what keeps `/load` safe to run on a machine you share.
|
|
125
|
+
- **Ten revisions is a guess.** Cheap to raise; it exists so a bad `/save` from
|
|
126
|
+
the wrong machine is recoverable at all.
|
|
127
|
+
- **A snapshot version bump.** Handled by refusing to read a newer snapshot and
|
|
128
|
+
naming `moshcode upgrade`, rather than by guessing at a shape this build has
|
|
129
|
+
never seen.
|
|
130
|
+
- **Should `/load` be able to pick a revision?** The app stores ten and the web
|
|
131
|
+
page can promote one, which covers recovery without adding a flag that takes a
|
|
132
|
+
number. Open if people ask for `--revision`.
|
package/prd/README.md
CHANGED
|
@@ -25,4 +25,5 @@ Start one with `moshcode prd "<idea>"` (TUI: `/prd`).
|
|
|
25
25
|
| [0007](0007-profullstack-site-init.md) | Generate batteries-included Profullstack sites for Moshpit names | Draft |
|
|
26
26
|
| [0008](0008-ticker-research-and-plugin-marketplace.md) | Bring equity research into the pit, and ship the pit's slash commands as a plugin | Draft |
|
|
27
27
|
| [0009](0009-persistent-agent-runtime.md) | Keep the herd alive — a persistent runtime, semantic agent state, and one control surface for humans and agents | Accepted |
|
|
28
|
+
| [0010](0010-cloud-settings-sync.md) | Sync the pit's settings to your moshcode.sh account | Draft |
|
|
28
29
|
<!-- PRD-INDEX:END -->
|
package/src/aliases.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Named shortcuts for whatever you type at the mosh prompt.
|
|
2
|
+
//
|
|
3
|
+
// The pit is a prompt people sit at all day, and the things they retype are
|
|
4
|
+
// their own: `git status`, `pnpm -r test`, `/agents claude --resume`. Shell
|
|
5
|
+
// aliases can't help — the pit is not a shell, and `!git status` is exactly the
|
|
6
|
+
// keystrokes an alias is supposed to save. So the pit keeps its own.
|
|
7
|
+
//
|
|
8
|
+
// An alias is a name and a line. The line is a shell command unless it starts
|
|
9
|
+
// with `/`, in which case it is a pit command:
|
|
10
|
+
//
|
|
11
|
+
// /alias set gs "git status" → /gs runs `$SHELL -c "git status"`
|
|
12
|
+
// /alias set cc "/agents claude" → /cc opens claude autonomously
|
|
13
|
+
//
|
|
14
|
+
// Shell-by-default because that is what the prompt is mostly asked for, and the
|
|
15
|
+
// leading slash is already how the pit spells its own verbs — so the rule reads
|
|
16
|
+
// the same way the rest of the pit does rather than being a new convention.
|
|
17
|
+
//
|
|
18
|
+
// Anything the pit can dispatch is fair game as a value, which is what keeps
|
|
19
|
+
// this from needing to grow a type: a bookmarklet or a URL becomes an alias the
|
|
20
|
+
// day the pit gets a verb that opens one, with no change here.
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
|
|
25
|
+
/** Owner-only, and for the same reason ~/.moshcode_history is: values are
|
|
26
|
+
* whatever was typed, and people alias commands that carry tokens. */
|
|
27
|
+
const FILE_MODE = 0o600;
|
|
28
|
+
|
|
29
|
+
const NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
30
|
+
|
|
31
|
+
/** A value long enough to be a pasted mistake rather than a command. */
|
|
32
|
+
const MAX_VALUE = 4096;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* How many times one line may expand before the pit gives up.
|
|
36
|
+
*
|
|
37
|
+
* Aliases can name aliases (`/alias set st "/gs --short"`), which is useful and
|
|
38
|
+
* also the one way to write a loop: two aliases naming each other would spin
|
|
39
|
+
* the dispatch loop forever. Ten is far past any chain a person builds on
|
|
40
|
+
* purpose.
|
|
41
|
+
*/
|
|
42
|
+
export const MAX_EXPANSIONS = 10;
|
|
43
|
+
|
|
44
|
+
/** Where the aliases live. Derived per call so tests can move $HOME. */
|
|
45
|
+
export function aliasFile() {
|
|
46
|
+
return path.join(os.homedir(), ".moshcode", "aliases.json");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every alias, as a plain name → line map.
|
|
51
|
+
*
|
|
52
|
+
* A file that is missing, unreadable, or not the shape we wrote reads as "no
|
|
53
|
+
* aliases" rather than throwing: this is called on the dispatch path for every
|
|
54
|
+
* unrecognised command, and a hand-edited file with a stray comma must not take
|
|
55
|
+
* the prompt down with it. Entries whose value is not a string are dropped for
|
|
56
|
+
* the same reason.
|
|
57
|
+
*/
|
|
58
|
+
export function loadAliases() {
|
|
59
|
+
let raw;
|
|
60
|
+
try { raw = fs.readFileSync(aliasFile(), "utf8"); }
|
|
61
|
+
catch { return {}; }
|
|
62
|
+
let parsed;
|
|
63
|
+
try { parsed = JSON.parse(raw); }
|
|
64
|
+
catch { return {}; }
|
|
65
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
68
|
+
if (typeof value === "string" && value.trim()) out[name.toLowerCase()] = value;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Write the map back, creating ~/.moshcode if this is the first alias. */
|
|
74
|
+
function saveAliases(aliases) {
|
|
75
|
+
const file = aliasFile();
|
|
76
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
77
|
+
// Sorted so the file reads like a list rather than like insertion order, and
|
|
78
|
+
// so hand edits produce a small diff.
|
|
79
|
+
const ordered = Object.fromEntries(Object.keys(aliases).sort().map((k) => [k, aliases[k]]));
|
|
80
|
+
fs.writeFileSync(file, `${JSON.stringify(ordered, null, 2)}\n`, { mode: FILE_MODE });
|
|
81
|
+
// `mode` only applies at creation, so an existing file keeps whatever the
|
|
82
|
+
// umask gave it. Tighten every write, the way the history file does.
|
|
83
|
+
try { fs.chmodSync(file, FILE_MODE); } catch { /* best effort */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The name as it is stored, or "" for anything that cannot be one. */
|
|
87
|
+
export function normalizeName(name) {
|
|
88
|
+
const clean = String(name ?? "").trim().toLowerCase().replace(/^\//, "");
|
|
89
|
+
return NAME_RE.test(clean) ? clean : "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One alias's line, or null. */
|
|
93
|
+
export function getAlias(name) {
|
|
94
|
+
const key = normalizeName(name);
|
|
95
|
+
if (!key) return null;
|
|
96
|
+
const aliases = loadAliases();
|
|
97
|
+
return Object.hasOwn(aliases, key) ? aliases[key] : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Define an alias. Returns { ok, error, name, value, previous }.
|
|
102
|
+
*
|
|
103
|
+
* `isReserved` asks the pit whether a name is already its own — a command, an
|
|
104
|
+
* engine, a tool. A predicate rather than a list because the dispatcher decides
|
|
105
|
+
* that by resolving, aliases included, and a list copied out of the rosters
|
|
106
|
+
* here would be a second answer that drifts from the first. A colliding name is
|
|
107
|
+
* refused rather than shadowed: built-ins are checked first, so an alias named
|
|
108
|
+
* `agents` would be silently dead, and a shortcut that does nothing is worse
|
|
109
|
+
* than one that was never accepted.
|
|
110
|
+
*/
|
|
111
|
+
export function setAlias(name, value, { isReserved = () => false } = {}) {
|
|
112
|
+
const key = normalizeName(name);
|
|
113
|
+
if (!key) {
|
|
114
|
+
return { ok: false, error: `"${name}" isn't a usable alias name — letters, digits, . _ - and it must start with a letter or digit` };
|
|
115
|
+
}
|
|
116
|
+
if (isReserved(key)) {
|
|
117
|
+
return { ok: false, error: `/${key} is already a pit command, engine, or tool — pick another name` };
|
|
118
|
+
}
|
|
119
|
+
const line = String(value ?? "").trim();
|
|
120
|
+
if (!line) return { ok: false, error: "an alias needs something to run" };
|
|
121
|
+
if (line.includes("\n")) return { ok: false, error: "an alias is a single line" };
|
|
122
|
+
if (line.length > MAX_VALUE) return { ok: false, error: `that value is ${line.length} characters — the cap is ${MAX_VALUE}` };
|
|
123
|
+
|
|
124
|
+
const aliases = loadAliases();
|
|
125
|
+
const previous = Object.hasOwn(aliases, key) ? aliases[key] : null;
|
|
126
|
+
aliases[key] = line;
|
|
127
|
+
try { saveAliases(aliases); }
|
|
128
|
+
catch (e) { return { ok: false, error: `can't write ${aliasFile()}: ${e.message}` }; }
|
|
129
|
+
return { ok: true, name: key, value: line, previous };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Forget one. Returns { ok, error, name, value }. */
|
|
133
|
+
export function removeAlias(name) {
|
|
134
|
+
const key = normalizeName(name);
|
|
135
|
+
const aliases = loadAliases();
|
|
136
|
+
if (!key || !Object.hasOwn(aliases, key)) {
|
|
137
|
+
return { ok: false, error: `no alias named "${String(name ?? "").replace(/^\//, "")}"` };
|
|
138
|
+
}
|
|
139
|
+
const value = aliases[key];
|
|
140
|
+
delete aliases[key];
|
|
141
|
+
try { saveAliases(aliases); }
|
|
142
|
+
catch (e) { return { ok: false, error: `can't write ${aliasFile()}: ${e.message}` }; }
|
|
143
|
+
return { ok: true, name: key, value };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The line an alias becomes, with anything else the user typed appended.
|
|
148
|
+
*
|
|
149
|
+
* Appended rather than substituted, the way a shell alias behaves: `/gs -sb` is
|
|
150
|
+
* `git status -sb`. `args` is the raw remainder of the typed line, not the
|
|
151
|
+
* tokenized parts, so the user's own quoting survives into `$SHELL -c`.
|
|
152
|
+
*
|
|
153
|
+
* The `!` is what routes a bare value to the shell — the pit already reads a
|
|
154
|
+
* leading `!` as "run this in $SHELL", so an alias does not need a second path
|
|
155
|
+
* through it.
|
|
156
|
+
*/
|
|
157
|
+
export function expandAlias(value, args = "") {
|
|
158
|
+
const line = `${String(value).trim()}${args ? ` ${args}` : ""}`;
|
|
159
|
+
return /^[/!]/.test(line) ? line : `!${line}`;
|
|
160
|
+
}
|
package/src/cli-schema.mjs
CHANGED
|
@@ -278,6 +278,43 @@ export const CORE_CLI_COMMANDS = [
|
|
|
278
278
|
synopsis: [["moshcode logout", ""]],
|
|
279
279
|
seeAlso: ["login"],
|
|
280
280
|
},
|
|
281
|
+
{
|
|
282
|
+
name: "save",
|
|
283
|
+
group: "account",
|
|
284
|
+
description: "save this machine's pit settings to your account",
|
|
285
|
+
synopsis: [["moshcode save [--dry-run] [--force] [--json]", ""]],
|
|
286
|
+
flags: [
|
|
287
|
+
["--dry-run", "list what would be saved and stop", ""],
|
|
288
|
+
["--force", "save even if another machine saved after this one last synced", ""],
|
|
289
|
+
["--json", "machine-readable result", ""],
|
|
290
|
+
],
|
|
291
|
+
examples: [
|
|
292
|
+
["moshcode save", "push aliases + herd rules to app.moshcode.sh"],
|
|
293
|
+
["moshcode save --dry-run", "what would go up"],
|
|
294
|
+
],
|
|
295
|
+
seeAlso: ["load", "login", "alias"],
|
|
296
|
+
note: "aliases (~/.moshcode/aliases.json) and herd rules (~/.moshcode/herd/rules.json). "
|
|
297
|
+
+ "credentials, live herd state and the package cache are never included. "
|
|
298
|
+
+ "each save is a numbered revision; the last ten are kept at app.moshcode.sh/settings/sync.",
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "load",
|
|
302
|
+
group: "account",
|
|
303
|
+
description: "bring your saved pit settings onto this machine",
|
|
304
|
+
synopsis: [["moshcode load [--dry-run] [--force] [--json]", ""]],
|
|
305
|
+
flags: [
|
|
306
|
+
["--dry-run", "show the per-file plan and change nothing", ""],
|
|
307
|
+
["--force", "overwrite local settings that changed since the last sync", ""],
|
|
308
|
+
["--json", "machine-readable result", ""],
|
|
309
|
+
],
|
|
310
|
+
examples: [
|
|
311
|
+
["moshcode load", "on a new machine, right after moshcode login"],
|
|
312
|
+
["moshcode load --dry-run", "which files would change"],
|
|
313
|
+
],
|
|
314
|
+
seeAlso: ["save", "login", "alias"],
|
|
315
|
+
note: "refuses rather than overwriting a local file you edited since the last sync — "
|
|
316
|
+
+ "`moshcode save` to keep it, or --force to replace it.",
|
|
317
|
+
},
|
|
281
318
|
{
|
|
282
319
|
name: "console",
|
|
283
320
|
group: "account",
|
|
@@ -891,10 +928,32 @@ export const PIT_COMMANDS = [
|
|
|
891
928
|
{ name: "whoami", cli: "whoami", description: "who this machine is logged in as" },
|
|
892
929
|
// Dispatched since forever and missing from /help until now.
|
|
893
930
|
{ name: "logout", cli: "logout", description: "clear the logged-in account" },
|
|
931
|
+
{ name: "save", args: "[--dry-run] [--force]", cli: "save",
|
|
932
|
+
description: "save this pit's settings to your moshcode.sh account" },
|
|
933
|
+
{ name: "load", args: "[--dry-run] [--force]", cli: "load",
|
|
934
|
+
description: "bring your saved settings onto this machine" },
|
|
894
935
|
{ name: "pwd", aliases: ["where"], cli: "pwd",
|
|
895
936
|
description: "show the current dir + git repo/branch/origin" },
|
|
896
937
|
{ name: "shell", aliases: ["sh"], args: "[cmd]", pitOnly: true,
|
|
897
938
|
description: "drop into $SHELL (exit → back to the pit); also !cmd" },
|
|
939
|
+
{ name: "alias", aliases: ["aliases"], args: 'set <name> "<cmd>" | list | get | rm', pitOnly: true,
|
|
940
|
+
description: "name a line you keep retyping; /<name> runs it",
|
|
941
|
+
synopsis: [
|
|
942
|
+
['/alias set <name> "<command>"', "define one (also: /alias <name> \"<command>\")"],
|
|
943
|
+
["/alias [list] [--json]", "every alias"],
|
|
944
|
+
["/alias get <name>", "what one expands to"],
|
|
945
|
+
["/alias rm <name>", "forget one"],
|
|
946
|
+
],
|
|
947
|
+
examples: [
|
|
948
|
+
['/alias set gs "git status"', "then /gs — and /gs -sb appends"],
|
|
949
|
+
// Deliberately not `cc`: that one is already how the pit spells claude,
|
|
950
|
+
// so the example would print a refusal for anyone who typed it.
|
|
951
|
+
['/alias set cx "/agents codex"', "a pit command, not a shell one"],
|
|
952
|
+
["/alias rm gs", ""],
|
|
953
|
+
],
|
|
954
|
+
note: "the command runs in $SHELL unless it starts with / — then it is a pit command. "
|
|
955
|
+
+ "Aliases live in ~/.moshcode/aliases.json and cannot shadow a pit command, engine, or tool.",
|
|
956
|
+
},
|
|
898
957
|
{ name: "help", aliases: ["?", "h"], args: "[command]", pitOnly: true,
|
|
899
958
|
description: "this, or one command in detail" },
|
|
900
959
|
{ name: "quit", aliases: ["exit", "q"], pitOnly: true,
|