deweyou-cli 0.2.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.
@@ -0,0 +1,353 @@
1
+ import { cachePaths, n as writeJson, t as readJson } from "./cache-CBEKVQeD.mjs";
2
+ import { cp, lstat, mkdir, readFile, realpath, rename, rm, symlink, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, isAbsolute, join, relative } from "node:path";
5
+ //#region src/cli/agents-md.ts
6
+ const DEWEYOU_SECTION_START = "<!-- deweyou-agent:start -->";
7
+ const DEWEYOU_SECTION_END = "<!-- deweyou-agent:end -->";
8
+ const DEWEY_SECTION = `${DEWEYOU_SECTION_START}
9
+ ## Dewey Workflow
10
+
11
+ This repository uses Dewey's personal agent workflow. Inspect \`.agents/\` before making changes, then run \`deweyou-cli agent context --format markdown\` and follow the returned rules, skill index, asset paths, and runtime notices.
12
+ ${DEWEYOU_SECTION_END}`;
13
+ async function upsertAgentsSection(repoRoot) {
14
+ const path = join(repoRoot, "AGENTS.md");
15
+ const next = upsertSection(await readAgentsFile(path));
16
+ await writeFile(path, next);
17
+ return next;
18
+ }
19
+ async function readAgentsFile(path) {
20
+ try {
21
+ return await readFile(path, "utf8");
22
+ } catch (error) {
23
+ if (!(error instanceof Error) || !("code" in error)) throw error;
24
+ if (error.code === "ENOENT") return "";
25
+ throw error;
26
+ }
27
+ }
28
+ function upsertSection(contents) {
29
+ const markedSection = new RegExp(`${escapeRegex(DEWEYOU_SECTION_START)}[\\s\\S]*?${escapeRegex(DEWEYOU_SECTION_END)}`);
30
+ if (markedSection.test(contents)) return ensureTrailingNewline(contents.replace(markedSection, DEWEY_SECTION));
31
+ const trimmed = contents.trimEnd();
32
+ if (!trimmed) return `${DEWEY_SECTION}\n`;
33
+ return `${trimmed}\n\n${DEWEY_SECTION}\n`;
34
+ }
35
+ function escapeRegex(value) {
36
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37
+ }
38
+ function ensureTrailingNewline(value) {
39
+ return value.endsWith("\n") ? value : `${value}\n`;
40
+ }
41
+ //#endregion
42
+ //#region src/cli/init.ts
43
+ const VALID_MODES = new Set([
44
+ "link",
45
+ "copy",
46
+ "pointer"
47
+ ]);
48
+ const SAFE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
49
+ async function initRepo({ repoRoot = process.cwd(), homeDir = homedir(), mode = "link", selected, force = false, dryRun = false } = {}) {
50
+ if (!selected) throw new Error("selected assets are required");
51
+ validateMode(mode);
52
+ const paths = cachePaths({ homeDir });
53
+ const registry = await readCachedRegistry(paths.assetsRoot);
54
+ const cacheManifest = await readCacheManifest(paths.manifestPath);
55
+ const assets = normalizeSelected(selected);
56
+ const agentsRoot = join(repoRoot, ".agents");
57
+ validateSelectedAssets(registry, assets);
58
+ const plan = await buildInitPlan({
59
+ repoRoot,
60
+ agentsRoot,
61
+ assetsRoot: paths.assetsRoot,
62
+ registry,
63
+ assets,
64
+ mode
65
+ });
66
+ if (mode !== "pointer") await validateDestinationSafety({
67
+ plan,
68
+ manifestPath: join(agentsRoot, "manifest.json"),
69
+ assetsRoot: paths.assetsRoot,
70
+ force
71
+ });
72
+ const manifest = {
73
+ mode,
74
+ source: cacheManifest.source,
75
+ cacheRoot: paths.assetsRoot,
76
+ assets,
77
+ assetSnapshot: snapshotSelectedAssetMetadata(registry, assets),
78
+ initializedAt: (/* @__PURE__ */ new Date()).toISOString()
79
+ };
80
+ if (dryRun) return {
81
+ dryRun: true,
82
+ mode,
83
+ source: cacheManifest.source,
84
+ cacheRoot: paths.assetsRoot,
85
+ assets,
86
+ assetSnapshot: snapshotSelectedAssetMetadata(registry, assets),
87
+ files: plan.files
88
+ };
89
+ await mkdir(agentsRoot, { recursive: true });
90
+ if (mode !== "pointer") await installAssets(plan.assets);
91
+ await writeJson(join(agentsRoot, "manifest.json"), manifest);
92
+ await upsertAgentsSection(repoRoot);
93
+ return manifest;
94
+ }
95
+ async function runInit(flags = {}, { promptForInit } = {}) {
96
+ const homeDir = flags.homeDir ?? homedir();
97
+ const repoRoot = flags.repoRoot ?? process.cwd();
98
+ const registry = await readCachedRegistry(cachePaths({ homeDir }).assetsRoot);
99
+ const scripted = hasScriptedSelectionFlags(flags);
100
+ let mode = flags.mode ?? "link";
101
+ let selected;
102
+ validateMode(mode);
103
+ if (flags.yes && !scripted) throw new Error("--yes requires --all, --skills, or --rules");
104
+ if (scripted) selected = flags.all ? selectAll(registry) : {
105
+ skills: flags.skills ?? [],
106
+ rules: flags.rules ?? []
107
+ };
108
+ else {
109
+ const prompted = await (promptForInit ?? await loadPromptForInit())({
110
+ registry,
111
+ repoRoot,
112
+ mode: flags.mode
113
+ });
114
+ mode = prompted.mode;
115
+ selected = prompted.selected;
116
+ }
117
+ if (!hasSelectedAssets(selected)) throw new Error("No assets selected. Pass --all, --skills, or --rules, or run interactive setup.");
118
+ const manifest = await initRepo({
119
+ repoRoot,
120
+ mode,
121
+ selected,
122
+ force: flags.force ?? false,
123
+ dryRun: flags.dryRun ?? false,
124
+ homeDir
125
+ });
126
+ if (flags.dryRun) {
127
+ console.log("Dewey workflow init plan:");
128
+ for (const file of manifest.files) console.log(`- ${file}`);
129
+ } else console.log("Initialized Dewey workflow for this repository.");
130
+ return manifest;
131
+ }
132
+ async function loadPromptForInit() {
133
+ const { promptForInit } = await import("./prompts-DVRcV560.mjs");
134
+ return promptForInit;
135
+ }
136
+ async function readCachedRegistry(assetsRoot) {
137
+ try {
138
+ const registry = JSON.parse(await readFile(join(assetsRoot, "registry.json"), "utf8"));
139
+ return {
140
+ ...registry,
141
+ assets: {
142
+ /* v8 ignore next -- legacy cache manifests may omit collections */
143
+ skills: registry.assets?.skills ?? {},
144
+ /* v8 ignore next -- legacy cache manifests may omit collections */
145
+ rules: registry.assets?.rules ?? {}
146
+ }
147
+ };
148
+ } catch (error) {
149
+ /* v8 ignore next -- defensive guard for non-Node filesystem errors */
150
+ if (!(error instanceof Error) || !("code" in error)) throw error;
151
+ if (error.code === "ENOENT") throw new Error("Dewey asset cache is missing. Run `deweyou-cli agent update` first.");
152
+ /* v8 ignore next -- invalid JSON and non-missing read errors should bubble */
153
+ throw error;
154
+ }
155
+ }
156
+ async function readCacheManifest(manifestPath) {
157
+ const manifest = await readJson(manifestPath, null);
158
+ if (!manifest?.source || typeof manifest.source.root !== "string") throw new Error("Dewey asset cache manifest is missing source metadata. Run `deweyou-cli agent update` first.");
159
+ return manifest;
160
+ }
161
+ function normalizeSelected(selected) {
162
+ return {
163
+ skills: [...selected.skills ?? []],
164
+ rules: [...selected.rules ?? []]
165
+ };
166
+ }
167
+ function hasSelectedAssets(selected) {
168
+ if (!selected) return false;
169
+ return selected.skills.length > 0 || selected.rules.length > 0;
170
+ }
171
+ function hasScriptedSelectionFlags(flags) {
172
+ return Boolean(flags.all || flags.skills || flags.rules);
173
+ }
174
+ function validateMode(mode) {
175
+ if (typeof mode !== "string") throw new Error("mode must be one of link, copy, or pointer");
176
+ if (!VALID_MODES.has(mode)) throw new Error("mode must be one of link, copy, or pointer");
177
+ }
178
+ function validateSelectedAssets(registry, selected) {
179
+ for (const skill of selected.skills) {
180
+ if (!registry.assets.skills[skill]) throw new Error(`Unknown Dewey skill: ${skill}`);
181
+ validateAssetId(skill, "skill");
182
+ }
183
+ for (const rule of selected.rules) {
184
+ if (!registry.assets.rules[rule]) throw new Error(`Unknown Dewey rule: ${rule}`);
185
+ validateAssetId(rule, "rule");
186
+ }
187
+ }
188
+ function validateAssetId(id, kind) {
189
+ if (!SAFE_ID.test(id)) throw new Error(`Dewey ${kind} id must be kebab-case: ${id}`);
190
+ }
191
+ function selectAll(registry) {
192
+ return {
193
+ skills: Object.keys(registry.assets.skills),
194
+ rules: Object.keys(registry.assets.rules)
195
+ };
196
+ }
197
+ function snapshotSelectedAssetMetadata(registry, selected) {
198
+ return {
199
+ skills: Object.fromEntries(selected.skills.map((name) => [name, pickAssetMetadata(registry.assets.skills[name])])),
200
+ rules: Object.fromEntries(selected.rules.map((name) => [name, pickAssetMetadata(registry.assets.rules[name])]))
201
+ };
202
+ }
203
+ function pickAssetMetadata(asset) {
204
+ return {
205
+ description: asset.description,
206
+ hash: asset.hash
207
+ };
208
+ }
209
+ async function buildInitPlan({ repoRoot, agentsRoot, assetsRoot, registry, assets, mode }) {
210
+ const assetPlans = mode === "pointer" ? [] : await Promise.all([...assets.skills.map((id) => buildAssetPlan({
211
+ kind: "skill",
212
+ id,
213
+ source: join(assetsRoot, registry.assets.skills[id].path),
214
+ destination: join(agentsRoot, "skills", id),
215
+ mode
216
+ })), ...assets.rules.map((id) => buildAssetPlan({
217
+ kind: "rule",
218
+ id,
219
+ source: join(assetsRoot, registry.assets.rules[id].path),
220
+ destination: join(agentsRoot, "rules", `${id}.md`),
221
+ mode
222
+ }))]);
223
+ return {
224
+ assets: assetPlans,
225
+ files: [
226
+ ...assetPlans.map((asset) => asset.destination),
227
+ join(agentsRoot, "manifest.json"),
228
+ join(repoRoot, "AGENTS.md")
229
+ ]
230
+ };
231
+ }
232
+ async function buildAssetPlan({ kind, id, source, destination, mode }) {
233
+ return {
234
+ kind,
235
+ id,
236
+ source: await realpath(source),
237
+ destination,
238
+ mode
239
+ };
240
+ }
241
+ async function validateDestinationSafety({ plan, manifestPath, assetsRoot, force }) {
242
+ const previousManifest = await readJson(manifestPath, null);
243
+ const safeManifestDestinations = new Set(manifestDestinations(dirname(manifestPath), previousManifest));
244
+ for (const asset of plan.assets) await validateDestination(asset, {
245
+ assetsRoot,
246
+ force,
247
+ safeManifestDestinations
248
+ });
249
+ }
250
+ function manifestDestinations(agentsRoot, manifest) {
251
+ if (!manifest?.assets) return [];
252
+ return [...(manifest.assets.skills ?? []).map((id) => join(agentsRoot, "skills", id)), ...(manifest.assets.rules ?? []).map((id) => join(agentsRoot, "rules", `${id}.md`))];
253
+ }
254
+ async function validateDestination(asset, { assetsRoot, force, safeManifestDestinations }) {
255
+ let destinationStat;
256
+ try {
257
+ destinationStat = await lstat(asset.destination);
258
+ } catch (error) {
259
+ /* v8 ignore next -- defensive guard for non-Node filesystem errors */
260
+ if (!(error instanceof Error) || !("code" in error)) throw error;
261
+ if (error.code === "ENOENT") return;
262
+ /* v8 ignore next -- non-missing lstat errors should bubble to the caller */
263
+ throw error;
264
+ }
265
+ if (!force) throw new Error(`${asset.destination} already exists. Use --force to replace Dewey-managed asset files.`);
266
+ if (safeManifestDestinations.has(asset.destination) || destinationStat.isSymbolicLink() && await pointsIntoAssetsRoot(asset.destination, assetsRoot)) return;
267
+ throw new Error(`Refusing to replace non-Dewey-managed destination: ${asset.destination}`);
268
+ }
269
+ async function pointsIntoAssetsRoot(destination, assetsRoot) {
270
+ let destinationTarget;
271
+ try {
272
+ destinationTarget = await realpath(destination);
273
+ } catch (error) {
274
+ /* v8 ignore next -- defensive guard for non-Node filesystem errors */
275
+ if (!(error instanceof Error) || !("code" in error)) throw error;
276
+ if (error.code === "ENOENT") return false;
277
+ /* v8 ignore next -- non-missing realpath errors should bubble to the caller */
278
+ throw error;
279
+ }
280
+ const pathFromRoot = relative(await realpath(assetsRoot), destinationTarget);
281
+ return pathFromRoot === "" || !pathFromRoot.startsWith("..") && !isAbsolute(pathFromRoot);
282
+ }
283
+ async function installAssets(assets) {
284
+ for (const asset of assets) await installAsset(asset);
285
+ }
286
+ async function installAsset({ source, destination, mode }) {
287
+ const parent = dirname(destination);
288
+ const tempDestination = tempSibling(destination, "tmp");
289
+ const backupDestination = tempSibling(destination, "backup");
290
+ await mkdir(parent, { recursive: true });
291
+ try {
292
+ await rm(tempDestination, {
293
+ recursive: true,
294
+ force: true
295
+ });
296
+ await rm(backupDestination, {
297
+ recursive: true,
298
+ force: true
299
+ });
300
+ if (mode === "link") await symlink(source, tempDestination);
301
+ else await cp(source, tempDestination, {
302
+ recursive: true,
303
+ force: false,
304
+ errorOnExist: true
305
+ });
306
+ const hasExistingDestination = await pathExists(destination);
307
+ if (hasExistingDestination) await rename(destination, backupDestination);
308
+ try {
309
+ await rename(tempDestination, destination);
310
+ } catch (error) {
311
+ /* v8 ignore next -- defensive guard for non-Error filesystem failures */
312
+ if (!(error instanceof Error)) throw error;
313
+ if (hasExistingDestination) await restoreBackup(backupDestination, destination, error);
314
+ throw error;
315
+ }
316
+ if (hasExistingDestination) await rm(backupDestination, {
317
+ recursive: true,
318
+ force: true
319
+ });
320
+ } catch (error) {
321
+ await rm(tempDestination, {
322
+ recursive: true,
323
+ force: true
324
+ });
325
+ /* v8 ignore next -- cleanup is deterministic; original error is rethrown */
326
+ throw error;
327
+ }
328
+ }
329
+ function tempSibling(destination, label) {
330
+ return join(dirname(destination), `.${basename(destination)}.${label}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
331
+ }
332
+ async function pathExists(path) {
333
+ try {
334
+ await lstat(path);
335
+ return true;
336
+ } catch (error) {
337
+ /* v8 ignore next -- defensive guard for non-Node filesystem errors */
338
+ if (!(error instanceof Error) || !("code" in error)) throw error;
339
+ if (error.code === "ENOENT") return false;
340
+ /* v8 ignore next -- non-missing lstat errors should bubble to the caller */
341
+ throw error;
342
+ }
343
+ }
344
+ async function restoreBackup(backupDestination, destination, originalError) {
345
+ try {
346
+ await rename(backupDestination, destination);
347
+ } catch (restoreError) {
348
+ /* v8 ignore next -- restore failures are attached to the original error */
349
+ originalError.restoreError = restoreError;
350
+ }
351
+ }
352
+ //#endregion
353
+ export { runInit };
@@ -0,0 +1,100 @@
1
+ import { cancel, confirm, intro, isCancel, multiselect, note, select } from "@clack/prompts";
2
+ //#region src/cli/prompts.ts
3
+ const SETUP_MODES = [
4
+ {
5
+ value: "link",
6
+ label: "link",
7
+ hint: "Symlink assets from the Dewey cache."
8
+ },
9
+ {
10
+ value: "copy",
11
+ label: "copy",
12
+ hint: "Copy asset files into this repository."
13
+ },
14
+ {
15
+ value: "pointer",
16
+ label: "pointer",
17
+ hint: "Write only the manifest and AGENTS.md pointers."
18
+ }
19
+ ];
20
+ const ASSET_SCOPES = [
21
+ {
22
+ value: "all",
23
+ label: "all",
24
+ hint: "Enable every cached skill and rule."
25
+ },
26
+ {
27
+ value: "custom",
28
+ label: "custom",
29
+ hint: "Choose skills and rules individually."
30
+ },
31
+ {
32
+ value: "skills",
33
+ label: "skills only",
34
+ hint: "Choose skills without installing rules."
35
+ },
36
+ {
37
+ value: "rules",
38
+ label: "rules only",
39
+ hint: "Choose rules without installing skills."
40
+ }
41
+ ];
42
+ async function promptForInit({ registry, repoRoot, mode }) {
43
+ intro("Dewey Agent Setup");
44
+ note(repoRoot, "Repository");
45
+ const selectedMode = mode ?? await promptOrExit(select({
46
+ message: "Select setup mode",
47
+ options: SETUP_MODES
48
+ }));
49
+ const selected = await selectAssets({
50
+ registry,
51
+ scope: await promptOrExit(select({
52
+ message: "Select asset scope",
53
+ options: ASSET_SCOPES
54
+ }))
55
+ });
56
+ if (!await promptOrExit(confirm({ message: `Enable ${selected.skills.length} skill(s) and ${selected.rules.length} rule(s) using ${selectedMode} mode?` }))) exitCancelled();
57
+ return {
58
+ mode: selectedMode,
59
+ selected
60
+ };
61
+ }
62
+ async function selectAssets({ registry, scope }) {
63
+ if (scope === "all") return {
64
+ skills: Object.keys(registry.assets.skills),
65
+ rules: Object.keys(registry.assets.rules)
66
+ };
67
+ const selected = {
68
+ skills: [],
69
+ rules: []
70
+ };
71
+ if (scope === "custom" || scope === "skills") selected.skills = await promptOrExit(multiselect({
72
+ message: "Select skills",
73
+ options: assetOptions(registry.assets.skills),
74
+ required: false
75
+ }));
76
+ if (scope === "custom" || scope === "rules") selected.rules = await promptOrExit(multiselect({
77
+ message: "Select rules",
78
+ options: assetOptions(registry.assets.rules),
79
+ required: false
80
+ }));
81
+ return selected;
82
+ }
83
+ function assetOptions(assets) {
84
+ return Object.entries(assets).map(([name, asset]) => ({
85
+ value: name,
86
+ label: name,
87
+ hint: asset.description
88
+ }));
89
+ }
90
+ async function promptOrExit(prompt) {
91
+ const value = await prompt;
92
+ if (isCancel(value)) exitCancelled();
93
+ return value;
94
+ }
95
+ function exitCancelled() {
96
+ cancel("Dewey agent setup cancelled.");
97
+ process.exit(0);
98
+ }
99
+ //#endregion
100
+ export { promptForInit };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "deweyou-cli",
3
+ "version": "0.2.1",
4
+ "type": "module",
5
+ "description": "Dewey's personal agent workflow bootstrapper",
6
+ "files": [
7
+ "dist",
8
+ "README.md",
9
+ "CHANGELOG.md"
10
+ ],
11
+ "bin": {
12
+ "deweyou-cli": "./dist/deweyou.mjs"
13
+ },
14
+ "scripts": {
15
+ "build": "vp pack src/bin/deweyou.ts --out-dir dist --clean",
16
+ "prepare": "npm run build",
17
+ "release:prepare": "tsx scripts/prepare-release.ts",
18
+ "test": "vp test run",
19
+ "test:coverage": "vitest run --coverage",
20
+ "typecheck": "tsc --noEmit",
21
+ "start": "tsx src/bin/deweyou.ts"
22
+ },
23
+ "dependencies": {
24
+ "@clack/prompts": "^0.11.0",
25
+ "js-yaml": "^4.1.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/js-yaml": "^4.0.9",
29
+ "@types/node": "^25.5.2",
30
+ "@vitest/coverage-v8": "4.1.5",
31
+ "typescript": "^6.0.2",
32
+ "tsx": "^4.21.0",
33
+ "vitest": "4.1.5",
34
+ "vite-plus": "^0.1.21"
35
+ },
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "license": "MIT"
40
+ }