@ajaykumarnpm/talea 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to this project are recorded here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the versions follow
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.4.0] - 2026-09-22
8
+
9
+ ### Added
10
+ - talea skill, and the CLI in the design system's palette
11
+
12
+ ### Fixed
13
+ - **test**: skill.test.js asserted LF against a CRLF checkout
14
+ - **test**: init.test.js depended on the author's own GitHub auth
15
+ - create group folders only when cloning
16
+
17
+ ### Documentation
18
+ - the site and the manual, at talea-run.web.app
19
+
7
20
  ## [0.3.0] - 2026-09-21
8
21
 
9
22
  ### Added
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  **One folder structure for every machine you work on.**
4
4
 
5
+ [talea-run.web.app](https://talea-run.web.app) — the site, and [the manual](https://talea-run.web.app/docs/).
6
+
5
7
  You have a laptop, a desktop, and a work machine. On each one, the repo you want
6
8
  is either missing or somewhere you have to go and find. `talea` fixes that: one
7
9
  catalogue of your GitHub repos, one tree, and a command that makes any machine
@@ -64,6 +66,21 @@ talea select # reopen the checklist and change the whole list
64
66
  cd $(talea where eklavya)
65
67
  ```
66
68
 
69
+ ## Let your coding agent do it
70
+
71
+ ```sh
72
+ talea skill install
73
+ ```
74
+
75
+ Installs a skill into Claude Code at **user scope**, so every project you open
76
+ has it. After that `"where is eklavya?"` and `"my repos are scattered, tidy them
77
+ up"` reach the right command — with the guardrails attached: an adopt is always
78
+ shown as a dry run first, `--loose` is never taken on your behalf, and a removal
79
+ is reported as *taken off the list* rather than as a delete.
80
+
81
+ `talea skill` says whether it is installed, `talea skill uninstall` takes it back
82
+ out, and it refuses to overwrite a skill called `talea` that talea did not write.
83
+
67
84
  ---
68
85
 
69
86
  ## The repos you already have
@@ -154,6 +171,7 @@ Treat the id like a bookmark you would not paste into a public channel.
154
171
  | `talea tree` | the folder tree on disk |
155
172
  | `talea exec -- <cmd>` | run one command in every repo |
156
173
  | `talea manifest push/pull` | move the catalogue between machines |
174
+ | `talea skill` | install the skill that lets your coding agent drive talea |
157
175
  | `talea doctor` | check this machine can do the work |
158
176
  | `talea upgrade` | update the CLI itself |
159
177
 
@@ -195,6 +213,14 @@ node bin/talea.js --help
195
213
 
196
214
  Tests run on macOS, Linux and Windows across Node 20, 22 and 24 on every push.
197
215
 
216
+ The website lives in `web/` and is its own thing — Astro, deployed to Firebase
217
+ Hosting on a push to `main` that touches it. `web/CLAUDE.md` is how to work on
218
+ it.
219
+
220
+ ```sh
221
+ cd web && npm install && npm run dev
222
+ ```
223
+
198
224
  ## Licence
199
225
 
200
226
  MIT.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ajaykumarnpm/talea",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "One folder structure for every machine. Clone, adopt and sync your GitHub repos from a manifest you own.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "bin",
17
17
  "src",
18
18
  "manifest",
19
+ "skills",
19
20
  "templates",
20
21
  "README.md",
21
22
  "CHANGELOG.md"
@@ -0,0 +1,172 @@
1
+ ---
2
+ name: talea
3
+ description: "Operate talea, the CLI that gives every machine the same folder structure for its GitHub repos. Use when the user asks where a repo is, to clone or sync or pull their repos, to tidy or reorganise a workspace, to adopt checkouts that are in the wrong place, to change what this machine keeps, or to move their repo catalogue to another machine. Also use before running a command in several repos at once. Do not use for ordinary git work inside one repo that is already checked out."
4
+ ---
5
+
6
+ <!-- talea-skill: installed by `talea skill install`; edits are overwritten -->
7
+
8
+ # talea
9
+
10
+ `talea` keeps one folder structure across every machine a developer works on.
11
+ It holds a catalogue of their GitHub repos, moves checkouts they already have
12
+ into place, clones what is missing, and fast-forwards the rest.
13
+
14
+ Two rules make it safe to hand to an agent: it never pushes, and it never
15
+ deletes. Everything below leans on that.
16
+
17
+ ## Find the binary first
18
+
19
+ ```bash
20
+ command -v talea
21
+ ```
22
+
23
+ That prints the path, or nothing. It is installed with
24
+ `npm install -g @ajaykumarnpm/talea` — the package is scoped, the command is
25
+ not. If it prints nothing, fall back to `npx -y @ajaykumarnpm/talea <command>`,
26
+ which downloads on first use. If that fails too, say talea is not installed and
27
+ give the one install command rather than guessing at a path.
28
+
29
+ Shell variables do not survive between tool calls, so write the resolved path
30
+ into every later command rather than setting `T=` and hoping.
31
+
32
+ Everything below writes `talea` for readability. Substitute whatever the line
33
+ above resolved to.
34
+
35
+ ## Where is this repo?
36
+
37
+ This is the command to reach for most, and the reason the tool exists.
38
+
39
+ ```bash
40
+ talea where eklavya # the absolute path, one line, nothing else
41
+ cd "$(talea where eklavya)" # what it is actually for
42
+ talea where # the workspace root
43
+ ```
44
+
45
+ It exits **non-zero** on an unknown name and writes every diagnostic to stderr,
46
+ so `cd "$(talea where typo)"` fails instead of landing in the home directory.
47
+ Never `cd` to a path it did not print.
48
+
49
+ Two owners can own a repo of the same name. When that happens it says so and
50
+ exits non-zero — pass `owner/name` rather than picking one.
51
+
52
+ ## Read before you write
53
+
54
+ None of these change anything on disk. Run one before proposing work.
55
+
56
+ | Command | Answers |
57
+ |---|---|
58
+ | `talea status` | branch, clean or dirty, ahead or behind, for every repo this machine keeps |
59
+ | `talea list` | the catalogue — every repo, its group, its folder |
60
+ | `talea tree` | the folder tree as it actually is on disk |
61
+ | `talea doctor` | can this machine do the work: git, SSH, GitHub auth, the workspace |
62
+ | `talea adopt` | what a move *would* do. It changes nothing without `--apply` |
63
+
64
+ `talea status` is the one to run before suggesting a sync — a dirty repo is
65
+ skipped, and saying so up front is better than reporting it afterwards.
66
+
67
+ ## Bringing a machine into line
68
+
69
+ ```bash
70
+ talea sync # clone what is missing, fast-forward what is there
71
+ talea clone # clone only — never fetches or merges
72
+ talea sync -g NonStop # one group
73
+ talea sync -r eklavya # one repo
74
+ ```
75
+
76
+ `sync` is safe to run unattended. It fast-forwards **the branch you are on** and
77
+ leaves anything else alone with a line saying why. It never switches branches,
78
+ never merges a divergence and never pushes.
79
+
80
+ Every bulk command exits non-zero if **any** repo failed, so check the exit
81
+ code — the per-repo errors are printed but the run keeps going.
82
+
83
+ ## Adoption: the one that moves things
84
+
85
+ A repo the developer already has, in the wrong folder, is **moved** into place
86
+ rather than re-cloned. A move keeps branches, stashes, the reflog and
87
+ uncommitted work; a re-clone throws all of it away.
88
+
89
+ ```bash
90
+ talea adopt # show what would move, change nothing
91
+ talea adopt --from ~/Desktop # look there too; the folder is remembered
92
+ talea adopt --apply # do it
93
+ ```
94
+
95
+ **Always run it without `--apply` first and show the developer the plan.** A
96
+ move relocates directories they may have open in an editor, a terminal or a
97
+ long-running process.
98
+
99
+ Matching is on the **git remote URL**, not the folder name. When only the name
100
+ matches — a fork, a mirror, an SDK cache — it is listed and left alone. `--loose`
101
+ or `-r <repo>` includes it. Do not reach for `--loose` on the developer's behalf:
102
+ the guard exists because an FVM Flutter SDK cache reports `origin` as
103
+ `flutter/flutter` and name-matches a personal fork, and adopting it breaks every
104
+ Flutter project on the machine.
105
+
106
+ A second copy of the same repo is **parked** in `.talea-duplicates/`, not
107
+ deleted. Say so when it happens — clearing that folder is the developer's call,
108
+ and nothing in talea will do it for them.
109
+
110
+ ## What this machine keeps
111
+
112
+ ```bash
113
+ talea add some-repo # keep one more here, and clone it now
114
+ talea rm some-repo # stop keeping it — the checkout stays exactly where it is
115
+ talea select # reopen the whole checklist, interactively
116
+ ```
117
+
118
+ `talea select` needs a terminal. In a non-interactive session it prints a
119
+ summary instead of hanging, so prefer `add` and `rm` when acting on the
120
+ developer's behalf and leave `select` as something to suggest they run.
121
+
122
+ `rm` does **not** delete the checkout. Say that plainly when you run it, or it
123
+ reads like data loss.
124
+
125
+ ## Running one command everywhere
126
+
127
+ ```bash
128
+ talea exec -- git status --short
129
+ talea exec -g NonStop -- npm test
130
+ ```
131
+
132
+ Everything after `--` runs in each repo. Treat it as you would any command run
133
+ in N repositories at once: read-only commands freely, anything that writes only
134
+ when the developer asked for it by name.
135
+
136
+ ## Moving the catalogue between machines
137
+
138
+ ```bash
139
+ talea manifest push # publish it; prints a gist id
140
+ talea manifest pull <gist-id> # on the next machine
141
+ ```
142
+
143
+ The gist is **private**, but it still carries the names of private
144
+ repositories. Treat the id as something not to paste into a public channel, and
145
+ do not print it into a message that is going somewhere shared.
146
+
147
+ The split that matters: the **catalogue** travels, the **selection** does not.
148
+ Pulling it onto a new laptop hands over the full list to choose from, never the
149
+ last machine's choices. So `manifest pull` alone changes nothing about what is
150
+ cloned — `talea init` or `talea select` after it is what fills the tree.
151
+
152
+ ## Narrowing any run
153
+
154
+ Every command takes `-g <group>` and `-r <repo>`, both repeatable, and `--help`
155
+ for its own examples. An unknown group or repo name **exits non-zero** rather
156
+ than quietly doing nothing, so a typo is loud.
157
+
158
+ ## Rules
159
+
160
+ - **Never `--apply` an adopt the developer has not seen the dry run of.**
161
+ - **Never `--loose`** unless they have looked at the name-only matches and said
162
+ which one they want.
163
+ - Report a move as *moved*, a removal as *taken off the list*, and a duplicate
164
+ as *parked*. Those are three different things and the words are the whole
165
+ safety story.
166
+ - Do not add `ignore: true` to a catalogue entry to work around a conflict
167
+ without saying so — it means another tool owns that checkout, and every talea
168
+ command will skip it from then on.
169
+ - talea never pushes. If a repo is ahead of its remote, report it and stop;
170
+ pushing is the developer's call and not talea's job.
171
+ - A bulk command that exits non-zero has a failed repo in it. Read the output
172
+ for the failures rather than reporting the run as done.
package/src/cli.js CHANGED
@@ -20,6 +20,7 @@ import * as manifest from './commands/manifest.js';
20
20
  import * as doctor from './commands/doctor.js';
21
21
  import * as exec from './commands/exec.js';
22
22
  import * as upgrade from './commands/upgrade.js';
23
+ import * as skill from './commands/skill.js';
23
24
  import { notifyIfOutdatedAsync } from './update.js';
24
25
 
25
26
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -41,6 +42,7 @@ const COMMANDS = {
41
42
  manifest,
42
43
  doctor,
43
44
  exec,
45
+ skill,
44
46
  upgrade,
45
47
  };
46
48
 
@@ -59,6 +61,7 @@ const ALIASES = {
59
61
  cd: 'where',
60
62
  path: 'where',
61
63
  run: 'exec',
64
+ skills: 'skill',
62
65
  remove: 'rm',
63
66
  'self-update': 'upgrade',
64
67
  };
@@ -115,6 +118,7 @@ ${c.bold('Commands')}
115
118
  ${c.cyan('exec')} run one command in every repo
116
119
  ${c.cyan('manifest')} push or pull the catalogue through a private gist
117
120
  ${c.cyan('doctor')} check git, SSH, GitHub auth and workspace health
121
+ ${c.cyan('skill')} install the skill that lets your coding agent drive talea
118
122
  ${c.cyan('upgrade')} update the CLI itself
119
123
 
120
124
  ${c.bold('A new machine')}
@@ -130,6 +134,9 @@ ${c.bold('Every day')}
130
134
  talea sync ${c.dim('# clone the new, fast-forward the rest')}
131
135
  talea status --drift ${c.dim('# what is not where I left it')}
132
136
 
137
+ ${c.bold('With an agent')}
138
+ talea skill install ${c.dim('# then ask Claude Code "where is eklavya?"')}
139
+
133
140
  ${c.bold('Common options')}
134
141
  -g, --group <names> restrict to groups, e.g. -g nonstopio
135
142
  -r, --repo <names> restrict to repos, e.g. -r eklavya
@@ -7,14 +7,11 @@ import {
7
7
  STATE_FILE,
8
8
  expandHome,
9
9
  findWorkspace,
10
- groupDir,
11
10
  loadManifest,
12
- loadState,
13
11
  repoGroup,
14
12
  saveState,
15
13
  } from '../config.js';
16
14
  import { samePath } from '../adopt.js';
17
- import { machineRepos } from '../workspace.js';
18
15
  import { c, context, fail, heading, info, ok, plain, skip, warn } from '../log.js';
19
16
  import { run as discover } from './discover.js';
20
17
  import { run as sync } from './sync.js';
@@ -129,13 +126,13 @@ export async function run(opts, positionals = []) {
129
126
  ['groups', `${groups.length}`],
130
127
  ]);
131
128
 
132
- // The group folders for the repos this machine will actually holdnot one
133
- // per owner in the catalogue. A collaborator repo you never clone should not
134
- // leave an empty folder in the tree forever.
135
- const mine = machineRepos(manifest, loadState(target));
136
- for (const g of new Set(mine.map((r) => repoGroup(r)))) {
137
- mkdirSync(path.join(target, groupDir(manifest, g)), { recursive: true });
138
- }
129
+ // No group folders are created here. `init` runs before the pickerthe
130
+ // checklist lives in `sync` so anything made at this point is made from the
131
+ // catalogue's defaults, which is every owner the discovery found. Picking
132
+ // four repos out of 228 then left 18 empty owner folders in the tree, and an
133
+ // empty folder is indistinguishable from a checkout somebody deleted.
134
+ // `cloneMissing` creates the parent of each repo it is about to clone, so the
135
+ // only folders that ever appear are the ones with something in them.
139
136
 
140
137
  if (opts.clone === false) {
141
138
  plain(`\n${c.dim('Workspace ready. Run `talea sync` when you want the repos.')}`);
@@ -0,0 +1,142 @@
1
+ // `talea skill` — put the agent-facing skill in the user's Claude Code.
2
+ //
3
+ // The skill is a single Markdown file describing how to drive this CLI safely:
4
+ // which commands read, which move things, and which two flags must never be
5
+ // reached for on somebody's behalf. Installing it at **user scope**
6
+ // (`~/.claude/skills/`) rather than in a repo is the point — the developer has
7
+ // one workspace across every project, so the agent needs the same instructions
8
+ // in all of them.
9
+
10
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ import { c, fail, heading, info, ok, plain, warn } from '../log.js';
16
+
17
+ const here = path.dirname(fileURLToPath(import.meta.url));
18
+
19
+ /** The copy that ships in the package. */
20
+ export const source = path.join(here, '..', '..', 'skills', 'talea', 'SKILL.md');
21
+
22
+ /**
23
+ * Claude Code reads user-scope skills from `~/.claude/skills/<name>/SKILL.md`.
24
+ * `CLAUDE_CONFIG_DIR` is how somebody moves that directory, and honouring it is
25
+ * the difference between installing the skill and installing it somewhere
26
+ * nothing will ever read it.
27
+ */
28
+ export const target = () =>
29
+ path.join(
30
+ process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'),
31
+ 'skills',
32
+ 'talea',
33
+ 'SKILL.md',
34
+ );
35
+
36
+ /**
37
+ * The marker that says a file at that path is ours.
38
+ *
39
+ * Somebody may already have their own skill called `talea`. Overwriting it
40
+ * would destroy work this tool has no claim on, so the only file `install`
41
+ * will replace is one carrying this line, and the only file `uninstall` will
42
+ * delete is the same.
43
+ */
44
+ const MARK = '<!-- talea-skill:';
45
+
46
+ export const isOurs = (file) => {
47
+ try {
48
+ return readFileSync(file, 'utf8').includes(MARK);
49
+ } catch {
50
+ return false;
51
+ }
52
+ };
53
+
54
+ export const help = `
55
+ ${c.bold('talea skill')} — teach your coding agent to drive talea
56
+
57
+ ${c.dim('talea skill')} is it installed, and where
58
+ ${c.dim('talea skill install')} copy it into ~/.claude/skills/talea/
59
+ ${c.dim('talea skill uninstall')} take it back out
60
+
61
+ Installs at ${c.bold('user scope')}, so every project you open gets it — your workspace
62
+ spans all of them, and the agent needs the same instructions in each.
63
+
64
+ The skill is what turns ${c.dim('"where is eklavya?"')} and ${c.dim('"tidy up my repos"')} into the
65
+ right talea command, with the guardrails attached: an adopt is always dry-run
66
+ first, ${c.dim('--loose')} is never taken on your behalf, and nothing is ever deleted.
67
+
68
+ It refuses to overwrite a skill called ${c.dim('talea')} that this tool did not write.
69
+ Move yours first if you have one.
70
+ `;
71
+
72
+ export function run(opts, positionals = []) {
73
+ const action = positionals[0] ?? 'status';
74
+ const dest = target();
75
+
76
+ if (action === 'status') return status(dest);
77
+ if (action === 'install') return install(dest);
78
+ if (action === 'uninstall' || action === 'remove') return uninstall(dest);
79
+
80
+ fail(`Unknown: talea skill ${action}`);
81
+ plain(`\n Try ${c.dim('talea skill install')}, ${c.dim('talea skill uninstall')}, or ${c.dim('talea skill')} on its own.`);
82
+ // `process.exitCode`, not `process.exit()` — the same contract `summary()`
83
+ // uses, and the only one a test can drive without taking the runner down.
84
+ process.exitCode = 1;
85
+ }
86
+
87
+ function status(dest) {
88
+ heading('talea skill');
89
+ if (!existsSync(dest)) {
90
+ warn('Not installed.');
91
+ plain(`\n ${c.dim('talea skill install')} put it in ${c.bold(dest)}`);
92
+ return;
93
+ }
94
+ if (!isOurs(dest)) {
95
+ warn(`Something else owns ${c.bold(dest)}.`);
96
+ plain(`\n It is not a file talea wrote, so talea will not touch it.`);
97
+ return;
98
+ }
99
+ const same = readFileSync(dest, 'utf8') === readFileSync(source, 'utf8');
100
+ ok(`Installed at ${c.bold(dest)}`);
101
+ if (!same) {
102
+ plain(`\n ${c.yellow('Out of date')} — ${c.dim('talea skill install')} rewrites it from this version.`);
103
+ }
104
+ }
105
+
106
+ function install(dest) {
107
+ heading('talea skill install');
108
+
109
+ if (existsSync(dest) && !isOurs(dest)) {
110
+ // Somebody's own skill, under the name we want. Never overwrite it: it is
111
+ // work this tool has no claim on, and there is no undo.
112
+ fail(`There is already a skill called "talea" at ${c.bold(dest)}, and talea did not write it.`);
113
+ plain(`\n Move it somewhere else first, then run this again. Nothing has been changed.`);
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+
118
+ mkdirSync(path.dirname(dest), { recursive: true });
119
+ copyFileSync(source, dest);
120
+ ok(`Installed ${c.bold(dest)}`);
121
+ info(`Restart Claude Code to pick it up — skills are read at session start.`);
122
+ plain(`\n Then ${c.dim('"where is eklavya?"')} or ${c.dim('"sync my repos"')} reaches the right command on its own.`);
123
+ }
124
+
125
+ function uninstall(dest) {
126
+ heading('talea skill uninstall');
127
+
128
+ if (!existsSync(dest)) {
129
+ info('Nothing to remove.');
130
+ return;
131
+ }
132
+ if (!isOurs(dest)) {
133
+ fail(`${c.bold(dest)} is not a file talea wrote — leaving it alone.`);
134
+ process.exitCode = 1;
135
+ return;
136
+ }
137
+ rmSync(dest, { force: true });
138
+ // The directory is ours too, and an empty one left behind shows up in
139
+ // `/skills` as a skill with no file.
140
+ rmSync(path.dirname(dest), { recursive: true, force: true });
141
+ ok(`Removed ${c.bold(dest)}`);
142
+ }
package/src/theme.js CHANGED
@@ -24,18 +24,23 @@ export const useColor =
24
24
  // fallback and uses it whenever COLORTERM is not set.
25
25
  const trueColor = /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? '');
26
26
 
27
- // ── Phosphor palette ───────────────────────────────────────────
28
- // [24-bit hex, basic-16 SGR code]. The basic code is what a terminal without
29
- // COLORTERM gets; it is deliberately the closest *readable* match rather than
30
- // the closest numerically `faint` has no bright equivalent, so it falls back
31
- // to grey rather than a green nobody can read on a light background.
27
+ // ── Verdigris palette ──────────────────────────────────────────
28
+ // [24-bit hex, basic-16 SGR code]. The same accent ramp the website is built
29
+ // on verdigris, new growth on aged bronze — so the CLI and talea.run are one
30
+ // product rather than two that share a name. The hexes are the `--vd-*` steps in
31
+ // `web/public/tokens.css`; change one and change the other.
32
+ //
33
+ // The basic code is what a terminal without COLORTERM gets. It is deliberately
34
+ // the closest *readable* match rather than the closest numerically — `faint`
35
+ // has no bright equivalent, so it falls back to grey rather than a teal nobody
36
+ // can read on a light background.
32
37
  const PALETTE = {
33
- green: ['#39ff14', 92], // hot phosphor — something worked
34
- aged: ['#1f8a3b', 32], // settled green — a branch, a path, a detail
35
- faint: ['#0e3b1c', 90], // barely lit — rules and fills
36
- amber: ['#ffb000', 33], // attention, but not a failure
37
- red: ['#ff3b30', 31], // failure
38
- bone: ['#d6dbd6', 37], // plain text that still wants to be lit
38
+ green: ['#79D5C4', 96], // --vd-300, the accent — something worked
39
+ aged: ['#199688', 36], // --vd-500 — a branch, a path, a detail
40
+ faint: ['#07403C', 90], // --vd-800 — rules and fills
41
+ amber: ['#E8920C', 33], // --warning: attention, but not a failure
42
+ red: ['#E5484D', 31], // --error: failure
43
+ bone: ['#EAE7E1', 37], // --ink: plain text that still wants to be lit
39
44
  };
40
45
 
41
46