@daldindev/agentic-skills 0.1.0 → 0.2.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
@@ -6,6 +6,17 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-09-05
10
+
11
+ ### Added
12
+
13
+ - `agentic-skills sync`: installs when the target is missing and updates when it is already there, so one command covers both states. It is the command for a `postinstall` hook, a CI step, or a container build, where the run cannot stop to ask which state it is in. Local edits are preserved as with `update`; `--force` overwrites them, so exit code 2 cannot happen.
14
+
15
+ ### Changed
16
+
17
+ - The note about a missing manifest is printed only when files were actually skipped, and now appears for every command rather than only `update`.
18
+ - The error `init` raises on a non-empty target points at `sync` for unattended runs.
19
+
9
20
  ## [0.1.0] - 2026-09-03
10
21
 
11
22
  ### Added
package/README.md CHANGED
@@ -31,7 +31,7 @@ npx agentic-skills init
31
31
  Requirements:
32
32
 
33
33
  - Node.js `>=20.11`
34
- - Network access to `codeload.github.com`, where GitHub serves source archives, when you run `init` or `update`
34
+ - Network access to `codeload.github.com`, where GitHub serves source archives, when you run `init`, `update`, or `sync`
35
35
 
36
36
  ## Quick Start
37
37
 
@@ -55,6 +55,7 @@ Point your assistant at `.agents/ARCHITECTURE.md`. It lists every component with
55
55
  | --- | --- |
56
56
  | `agentic-skills init` | Download ag-kit and install the content. Refuses a non-empty target unless `--force` |
57
57
  | `agentic-skills update` | Download ag-kit again and update installed files, keeping the ones you edited |
58
+ | `agentic-skills sync` | Install if missing, update if present. One command that works in both states, for unattended runs |
58
59
  | `agentic-skills status` | Show what is installed, from which upstream commit, and which files were changed locally |
59
60
 
60
61
  | Option | Purpose |
@@ -92,6 +93,28 @@ To stay on a known upstream state instead of `main`, pass a tag or commit. The s
92
93
  npx @daldindev/agentic-skills update --ref v2026.8.31
93
94
  ```
94
95
 
96
+ ### Unattended runs
97
+
98
+ `sync` reconciles the install against upstream whatever state it is in: it creates the directory when it is missing and updates it when it is already there. Nothing has to be known about the target beforehand, which is what makes it the command to put where no one is watching — a `postinstall` hook, a CI step, a container build:
99
+
100
+ ```json
101
+ {
102
+ "scripts": {
103
+ "postinstall": "agentic-skills sync"
104
+ }
105
+ }
106
+ ```
107
+
108
+ Every `npm i` then brings the install to the current upstream, on a fresh clone and on a checkout that already has the content alike.
109
+
110
+ Your edits survive it: a file you changed is kept, listed, and the command exits `2`. Reach for `--force` only when the install is a derived tree your project regenerates and never edits by hand, `.agents/` in `.gitignore` for instance:
111
+
112
+ ```bash
113
+ agentic-skills sync --force
114
+ ```
115
+
116
+ Copying that second line into a project where `.agents/` is committed means losing an edited skill on every `npm i`, silently. And `--force` only removes exit `2`: a download that cannot reach GitHub still exits `1`.
117
+
95
118
  ## What gets installed
96
119
 
97
120
  | Upstream path | Installed as |
@@ -127,6 +150,7 @@ In scope:
127
150
  - Downloading the upstream roles, skills, and workflows and installing them anywhere
128
151
  - Updating an install while preserving local edits
129
152
  - Recording upstream provenance with every install
153
+ - Running unattended, from a postinstall hook or a CI step, with one command that works in either state
130
154
 
131
155
  Out of scope:
132
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daldindev/agentic-skills",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Install the ag-kit agent roles, skills, and workflows into any project, for any model and any agent harness.",
5
5
  "author": "daldindev",
6
6
  "license": "MIT",
package/src/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { parseArgs } from "node:util";
6
6
  import { PACKAGE_ROOT, readJson } from "./fs-utils.mjs";
7
- import { DEFAULT_INSTALL_DIR, init, status, update } from "./install.mjs";
7
+ import { DEFAULT_INSTALL_DIR, init, status, sync, update } from "./install.mjs";
8
8
  import { UPSTREAM, extractPort, loadArchive, materialize } from "./upstream.mjs";
9
9
 
10
10
  const HELP = `agentic-skills - install the ag-kit agent roles, skills, and workflows into any project
@@ -12,6 +12,7 @@ const HELP = `agentic-skills - install the ag-kit agent roles, skills, and workf
12
12
  Usage:
13
13
  agentic-skills init [options] Download ag-kit and install the content into <path>/<dir>
14
14
  agentic-skills update [options] Download ag-kit again and update installed files, preserving local edits
15
+ agentic-skills sync [options] Install if missing, update if present; safe to run unattended
15
16
  agentic-skills status [options] Show what is installed and which files were changed locally
16
17
 
17
18
  Options:
@@ -100,10 +101,10 @@ const printPlan = (result) => {
100
101
  );
101
102
  list("Skipped (modified locally; use --force to overwrite)", plan.skip, (item) => `${item.file}: ${item.reason}`);
102
103
  list("Kept (removed upstream but modified locally)", plan.keep, (item) => `${item.file}: ${item.reason}`);
103
- if (result.mode === "update" && result.hadManifest === false) {
104
+ if (result.hadManifest === false && plan.skip.length) {
104
105
  console.log("\nNo manifest was found, so files that differ from upstream were skipped. Re-run with --force to overwrite them.");
105
106
  }
106
- if (result.mode === "init" && !result.dryRun) {
107
+ if (result.created && !result.dryRun) {
107
108
  console.log(`\nPoint your assistant at ${path.join(result.installDir, "ARCHITECTURE.md")} to get the inventory.`);
108
109
  }
109
110
  };
@@ -133,7 +134,7 @@ try {
133
134
  console.log(pkg.version);
134
135
  } else if (values.help || !command || command === "help") {
135
136
  process.stdout.write(HELP);
136
- } else if (command === "init" || command === "update") {
137
+ } else if (command === "init" || command === "update" || command === "sync") {
137
138
  const incoming = await fetchIncoming();
138
139
  try {
139
140
  const options = {
@@ -144,7 +145,8 @@ try {
144
145
  incomingDir: incoming.dir,
145
146
  upstream: incoming.upstream,
146
147
  };
147
- const result = command === "init" ? await init(options) : await update(options);
148
+ const run = { init, update, sync }[command];
149
+ const result = await run(options);
148
150
  if (values.json) console.log(JSON.stringify(result, null, 2));
149
151
  else printPlan(result);
150
152
  if (result.plan.skip.length || result.plan.keep.length) process.exitCode = 2;
package/src/install.mjs CHANGED
@@ -120,49 +120,64 @@ async function hasContent(dir) {
120
120
  return entries.length > 0;
121
121
  }
122
122
 
123
- export async function init(options = {}) {
123
+ /**
124
+ * Plan against the incoming tree, apply it, and record the manifest.
125
+ * The three commands differ only in the preconditions they enforce first.
126
+ */
127
+ async function reconcile(mode, options) {
124
128
  const { installDir } = resolveTarget(options);
125
129
  const { incomingDir, upstream = null } = options;
126
- const force = Boolean(options.force);
127
-
128
- if (!force && (await hasContent(installDir))) {
129
- throw new Error(
130
- `${installDir} already exists and is not empty. Run "agentic-skills update" to update it, ` +
131
- "or pass --force to overwrite managed files in place.",
132
- );
133
- }
134
130
 
131
+ const created = !(await hasContent(installDir));
135
132
  const manifest = await loadManifest(installDir);
136
- const plan = await planUpdate({ installDir, incomingDir, manifest, force });
133
+ const plan = await planUpdate({ installDir, incomingDir, manifest, force: Boolean(options.force) });
137
134
  if (!options.dryRun) {
138
135
  await applyPlan({ installDir, incomingDir, plan });
139
136
  await writeManifest(installDir, plan.incoming, { previous: manifest, upstream });
140
137
  }
141
- return { mode: "init", installDir, plan, upstream, dryRun: Boolean(options.dryRun) };
138
+ return {
139
+ mode,
140
+ installDir,
141
+ plan,
142
+ upstream,
143
+ created,
144
+ hadManifest: Boolean(manifest),
145
+ dryRun: Boolean(options.dryRun),
146
+ };
147
+ }
148
+
149
+ export async function init(options = {}) {
150
+ const { installDir } = resolveTarget(options);
151
+
152
+ if (!options.force && (await hasContent(installDir))) {
153
+ throw new Error(
154
+ `${installDir} already exists and is not empty. Run "agentic-skills update" to update it, ` +
155
+ '"agentic-skills sync" if this runs unattended, or pass --force to overwrite managed files in place.',
156
+ );
157
+ }
158
+
159
+ return reconcile("init", options);
142
160
  }
143
161
 
144
162
  export async function update(options = {}) {
145
163
  const { installDir } = resolveTarget(options);
146
- const { incomingDir, upstream = null } = options;
147
164
 
148
165
  if (!(await exists(installDir))) {
149
166
  throw new Error(`${installDir} does not exist. Run "agentic-skills init" first.`);
150
167
  }
151
168
 
152
- const manifest = await loadManifest(installDir);
153
- const plan = await planUpdate({ installDir, incomingDir, manifest, force: Boolean(options.force) });
154
- if (!options.dryRun) {
155
- await applyPlan({ installDir, incomingDir, plan });
156
- await writeManifest(installDir, plan.incoming, { previous: manifest, upstream });
157
- }
158
- return {
159
- mode: "update",
160
- installDir,
161
- plan,
162
- upstream,
163
- hadManifest: Boolean(manifest),
164
- dryRun: Boolean(options.dryRun),
165
- };
169
+ return reconcile("update", options);
170
+ }
171
+
172
+ /**
173
+ * Reconcile whatever state the target is in: install it when missing, update it
174
+ * when it is already there. Neither precondition applies, so this is the command
175
+ * to run unattended - a postinstall, a CI step, a container build - where one
176
+ * line has to work on a fresh clone and on an existing tree alike. Local edits
177
+ * are preserved exactly as with update, and --force overwrites them.
178
+ */
179
+ export async function sync(options = {}) {
180
+ return reconcile("sync", options);
166
181
  }
167
182
 
168
183
  /** Local report only; nothing is fetched. */