devlyn-cli 4.2.0 → 4.2.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/README.md +4 -2
- package/bin/devlyn.js +184 -147
- package/bin/skill-history.json +140 -0
- package/bin/skill-ownership.js +67 -0
- package/node_modules/@inquirer/ansi/LICENSE +22 -0
- package/node_modules/@inquirer/ansi/README.md +121 -0
- package/node_modules/@inquirer/ansi/dist/index.d.ts +14 -0
- package/node_modules/@inquirer/ansi/dist/index.js +21 -0
- package/node_modules/@inquirer/ansi/package.json +79 -0
- package/node_modules/@inquirer/checkbox/LICENSE +22 -0
- package/node_modules/@inquirer/checkbox/README.md +265 -0
- package/node_modules/@inquirer/checkbox/dist/index.d.ts +67 -0
- package/node_modules/@inquirer/checkbox/dist/index.js +210 -0
- package/node_modules/@inquirer/checkbox/package.json +93 -0
- package/node_modules/@inquirer/core/LICENSE +22 -0
- package/node_modules/@inquirer/core/README.md +424 -0
- package/node_modules/@inquirer/core/dist/index.d.ts +13 -0
- package/node_modules/@inquirer/core/dist/index.js +12 -0
- package/node_modules/@inquirer/core/dist/lib/Separator.d.ts +10 -0
- package/node_modules/@inquirer/core/dist/lib/Separator.js +21 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p1.d.ts +9 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p1.js +4 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p2.d.ts +9 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p2.js +3 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p3.d.ts +9 -0
- package/node_modules/@inquirer/core/dist/lib/__probe/p3.js +5 -0
- package/node_modules/@inquirer/core/dist/lib/create-prompt.d.ts +11 -0
- package/node_modules/@inquirer/core/dist/lib/create-prompt.js +185 -0
- package/node_modules/@inquirer/core/dist/lib/errors.d.ts +20 -0
- package/node_modules/@inquirer/core/dist/lib/errors.js +21 -0
- package/node_modules/@inquirer/core/dist/lib/hook-engine.d.ts +17 -0
- package/node_modules/@inquirer/core/dist/lib/hook-engine.js +109 -0
- package/node_modules/@inquirer/core/dist/lib/key.d.ts +15 -0
- package/node_modules/@inquirer/core/dist/lib/key.js +34 -0
- package/node_modules/@inquirer/core/dist/lib/make-theme.d.ts +3 -0
- package/node_modules/@inquirer/core/dist/lib/make-theme.js +33 -0
- package/node_modules/@inquirer/core/dist/lib/pagination/use-pagination.d.ts +15 -0
- package/node_modules/@inquirer/core/dist/lib/pagination/use-pagination.js +121 -0
- package/node_modules/@inquirer/core/dist/lib/screen-manager.d.ts +14 -0
- package/node_modules/@inquirer/core/dist/lib/screen-manager.js +84 -0
- package/node_modules/@inquirer/core/dist/lib/theme.d.ts +166 -0
- package/node_modules/@inquirer/core/dist/lib/theme.js +29 -0
- package/node_modules/@inquirer/core/dist/lib/use-effect.d.ts +2 -0
- package/node_modules/@inquirer/core/dist/lib/use-effect.js +11 -0
- package/node_modules/@inquirer/core/dist/lib/use-keypress.d.ts +3 -0
- package/node_modules/@inquirer/core/dist/lib/use-keypress.js +20 -0
- package/node_modules/@inquirer/core/dist/lib/use-memo.d.ts +1 -0
- package/node_modules/@inquirer/core/dist/lib/use-memo.js +14 -0
- package/node_modules/@inquirer/core/dist/lib/use-prefix.d.ts +5 -0
- package/node_modules/@inquirer/core/dist/lib/use-prefix.js +35 -0
- package/node_modules/@inquirer/core/dist/lib/use-ref.d.ts +6 -0
- package/node_modules/@inquirer/core/dist/lib/use-ref.js +4 -0
- package/node_modules/@inquirer/core/dist/lib/use-state.d.ts +7 -0
- package/node_modules/@inquirer/core/dist/lib/use-state.js +28 -0
- package/node_modules/@inquirer/core/dist/lib/utils.d.ts +13 -0
- package/node_modules/@inquirer/core/dist/lib/utils.js +25 -0
- package/node_modules/@inquirer/core/package.json +98 -0
- package/node_modules/@inquirer/figures/LICENSE +22 -0
- package/node_modules/@inquirer/figures/dist/index.d.ts +275 -0
- package/node_modules/@inquirer/figures/dist/index.js +315 -0
- package/node_modules/@inquirer/figures/package.json +81 -0
- package/node_modules/@inquirer/select/LICENSE +22 -0
- package/node_modules/@inquirer/select/README.md +258 -0
- package/node_modules/@inquirer/select/dist/index.d.ts +42 -0
- package/node_modules/@inquirer/select/dist/index.js +191 -0
- package/node_modules/@inquirer/select/package.json +93 -0
- package/node_modules/@inquirer/type/LICENSE +22 -0
- package/node_modules/@inquirer/type/dist/index.d.ts +2 -0
- package/node_modules/@inquirer/type/dist/index.js +2 -0
- package/node_modules/@inquirer/type/dist/inquirer.d.ts +35 -0
- package/node_modules/@inquirer/type/dist/inquirer.js +1 -0
- package/node_modules/@inquirer/type/dist/utils.d.ts +45 -0
- package/node_modules/@inquirer/type/dist/utils.js +16 -0
- package/node_modules/@inquirer/type/package.json +88 -0
- package/node_modules/cli-width/LICENSE +13 -0
- package/node_modules/cli-width/README.md +71 -0
- package/node_modules/cli-width/index.d.ts +13 -0
- package/node_modules/cli-width/index.js +49 -0
- package/node_modules/cli-width/package.json +40 -0
- package/node_modules/fast-string-truncated-width/dist/index.d.ts +4 -0
- package/node_modules/fast-string-truncated-width/dist/index.js +111 -0
- package/node_modules/fast-string-truncated-width/dist/types.d.ts +19 -0
- package/node_modules/fast-string-truncated-width/dist/types.js +2 -0
- package/node_modules/fast-string-truncated-width/dist/utils.d.ts +4 -0
- package/node_modules/fast-string-truncated-width/dist/utils.js +20 -0
- package/node_modules/fast-string-truncated-width/license +21 -0
- package/node_modules/fast-string-truncated-width/package.json +35 -0
- package/node_modules/fast-string-truncated-width/readme.md +59 -0
- package/node_modules/fast-string-width/dist/index.d.ts +4 -0
- package/node_modules/fast-string-width/dist/index.js +14 -0
- package/node_modules/fast-string-width/license +21 -0
- package/node_modules/fast-string-width/package.json +34 -0
- package/node_modules/fast-string-width/readme.md +42 -0
- package/node_modules/fast-wrap-ansi/LICENSE +23 -0
- package/node_modules/fast-wrap-ansi/README.md +26 -0
- package/node_modules/fast-wrap-ansi/lib/main.d.ts +6 -0
- package/node_modules/fast-wrap-ansi/lib/main.js +218 -0
- package/node_modules/fast-wrap-ansi/package.json +51 -0
- package/node_modules/mute-stream/LICENSE +15 -0
- package/node_modules/mute-stream/README.md +68 -0
- package/node_modules/mute-stream/lib/index.js +142 -0
- package/node_modules/mute-stream/package.json +54 -0
- package/node_modules/signal-exit/LICENSE.txt +16 -0
- package/node_modules/signal-exit/README.md +74 -0
- package/node_modules/signal-exit/dist/cjs/browser.d.ts +12 -0
- package/node_modules/signal-exit/dist/cjs/browser.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/cjs/browser.js +10 -0
- package/node_modules/signal-exit/dist/cjs/browser.js.map +1 -0
- package/node_modules/signal-exit/dist/cjs/index.d.ts +48 -0
- package/node_modules/signal-exit/dist/cjs/index.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/cjs/index.js +279 -0
- package/node_modules/signal-exit/dist/cjs/index.js.map +1 -0
- package/node_modules/signal-exit/dist/cjs/package.json +3 -0
- package/node_modules/signal-exit/dist/cjs/signals.d.ts +29 -0
- package/node_modules/signal-exit/dist/cjs/signals.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/cjs/signals.js +42 -0
- package/node_modules/signal-exit/dist/cjs/signals.js.map +1 -0
- package/node_modules/signal-exit/dist/mjs/browser.d.ts +12 -0
- package/node_modules/signal-exit/dist/mjs/browser.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/mjs/browser.js +4 -0
- package/node_modules/signal-exit/dist/mjs/browser.js.map +1 -0
- package/node_modules/signal-exit/dist/mjs/index.d.ts +48 -0
- package/node_modules/signal-exit/dist/mjs/index.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/mjs/index.js +275 -0
- package/node_modules/signal-exit/dist/mjs/index.js.map +1 -0
- package/node_modules/signal-exit/dist/mjs/package.json +3 -0
- package/node_modules/signal-exit/dist/mjs/signals.d.ts +29 -0
- package/node_modules/signal-exit/dist/mjs/signals.d.ts.map +1 -0
- package/node_modules/signal-exit/dist/mjs/signals.js +39 -0
- package/node_modules/signal-exit/dist/mjs/signals.js.map +1 -0
- package/node_modules/signal-exit/package.json +106 -0
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -215,10 +215,12 @@ Selected during install. Run `npx devlyn-cli` again to add more.
|
|
|
215
215
|
|
|
216
216
|
---
|
|
217
217
|
|
|
218
|
+
Installer updates preserve user work: before writing any selected target, devlyn refuses to replace or remove git-tracked skills, edited skills, or directories with extra files (including hidden files). `-y` does not bypass this check. Commit your work or move conflicting folders to a backup and migrate them manually; tracked folders must be moved out of the install path before rerunning. Unchanged installer copies and pristine untracked 4.1.0 installs upgrade automatically. Older copies without verifiable content history require manual migration.
|
|
219
|
+
|
|
218
220
|
## Requirements
|
|
219
221
|
|
|
220
|
-
- **Node.js
|
|
221
|
-
- **Python 3.11+** available as `python3
|
|
222
|
+
- **Node.js 20 (20.17+), 22 (22.13+) or 23.5+** and npm
|
|
223
|
+
- **Python 3.11+** available as `python3` for the harness, and **Git** for safe installation and the harness
|
|
222
224
|
- **An agent CLI** installed and configured: Codex, omp, Pi or Grok (AGENTS.md), or [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (CLAUDE.md)
|
|
223
225
|
- **For `auto`/`pr` delivery**, an `origin` remote naming one GitHub repository and an authenticated [`gh`](https://cli.github.com/)
|
|
224
226
|
|
package/bin/devlyn.js
CHANGED
|
@@ -4,13 +4,14 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
-
const readline = require('readline');
|
|
8
7
|
const { execSync } = require('child_process');
|
|
9
8
|
|
|
10
9
|
const CONFIG_SOURCE = path.join(__dirname, '..', 'config');
|
|
11
10
|
const OPTIONAL_SKILLS_SOURCE = path.join(__dirname, '..', 'optional-skills');
|
|
12
11
|
const PKG = require('../package.json');
|
|
13
12
|
const { updateInstructions, InstructionError, holdsDevlynDefaults, importsAgentsMd } = require('./instructions');
|
|
13
|
+
const { contentHash, fingerprint, isOsMetadata, trackedPaths } = require('./skill-ownership');
|
|
14
|
+
const SKILL_HISTORY = require('./skill-history.json');
|
|
14
15
|
|
|
15
16
|
// The devlyn skill bundle installed into every skill-capable agent's loader
|
|
16
17
|
// directory. Single source of truth so codex/omp/pi stay in lockstep — adding a
|
|
@@ -82,13 +83,13 @@ const RETIRED_SKILL_MD_SHA256 = {
|
|
|
82
83
|
]),
|
|
83
84
|
};
|
|
84
85
|
|
|
85
|
-
// Whether `dir` is a real folder whose only file
|
|
86
|
+
// Whether `dir` is a real folder whose only file, besides OS metadata, is a SKILL.md
|
|
86
87
|
// with an LF-normalized SHA-256 in `hashes`: an unedited copy devlyn-cli shipped, not something
|
|
87
88
|
// the user added to or wrote.
|
|
88
89
|
function isShippedCopy(dir, hashes) {
|
|
89
90
|
const skill = path.join(dir, 'SKILL.md');
|
|
90
91
|
return fs.lstatSync(dir, { throwIfNoEntry: false })?.isDirectory() === true
|
|
91
|
-
&& fs.readdirSync(dir).filter((name) => !
|
|
92
|
+
&& fs.readdirSync(dir).filter((name) => !isOsMetadata(path.join(dir, name))).length === 1
|
|
92
93
|
&& fs.lstatSync(skill, { throwIfNoEntry: false })?.isFile() === true
|
|
93
94
|
&& hashes.has(crypto.createHash('sha256')
|
|
94
95
|
.update(fs.readFileSync(skill, 'utf8').replace(/\r\n/g, '\n')).digest('hex'));
|
|
@@ -121,7 +122,8 @@ function hasDevlynClaude(global) {
|
|
|
121
122
|
|| holdsDevlynDefaults('CLAUDE.md', claude) || importsAgentsDefaults(claude);
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
// Commands removed in previous versions; the project Claude install deletes them from .claude
|
|
125
|
+
// Commands removed in previous versions; the project Claude install deletes them from .claude/
|
|
126
|
+
// when they hold the bytes a version shipped (SKILL_HISTORY.commands).
|
|
125
127
|
const DEPRECATED_FILES = [
|
|
126
128
|
'commands/devlyn.handoff.md', // removed in v0.2.0
|
|
127
129
|
'commands/devlyn.clean.md', // migrated to skills in v0.6.0
|
|
@@ -146,7 +148,7 @@ const DEPRECATED_FILES = [
|
|
|
146
148
|
// Skill directories renamed from devlyn-* to devlyn:* in v0.7.x, plus
|
|
147
149
|
// iter-0034 Phase 4 cutover (2026-05-03): 15 user skills deleted and 3 moved
|
|
148
150
|
// to optional-skills/. Listed here so post-cutover `npx devlyn-cli` upgrades
|
|
149
|
-
//
|
|
151
|
+
// remove verified, unchanged legacy skill dirs from downstream `~/.claude/skills/`
|
|
150
152
|
// even though the source dirs no longer exist (the installer replaces only
|
|
151
153
|
// the skills it ships — without this list, deleted-from-source skills persist
|
|
152
154
|
// in user installs forever).
|
|
@@ -374,15 +376,22 @@ function cleanupDeprecated(skillsDir) {
|
|
|
374
376
|
// copy is refreshed in place. Any other folder under a 4.0 name is the user's own; `-y` leaves it.
|
|
375
377
|
function retireRenamedSkills(skillsDir) {
|
|
376
378
|
const optional = new Set(OPTIONAL_ADDONS.filter((addon) => addon.type === 'local').map((addon) => addon.name));
|
|
379
|
+
const refreshed = [];
|
|
377
380
|
for (const [oldName, newName] of Object.entries(RENAMED_SKILLS)) {
|
|
378
381
|
const found = legacySkillPaths(skillsDir, oldName).filter((fullPath) => fs.existsSync(fullPath));
|
|
379
|
-
if (found.length > 0 && optional.has(newName))
|
|
380
|
-
|
|
382
|
+
if (found.length > 0 && optional.has(newName)) {
|
|
383
|
+
installOptionalSkillInto(skillsDir, newName);
|
|
384
|
+
refreshed.push(newName);
|
|
385
|
+
} else if (isShippedCopy(path.join(skillsDir, newName), PRE_STANDARD_SKILL_MD_SHA256)) {
|
|
386
|
+
refreshPreStandardCopy(skillsDir, newName);
|
|
387
|
+
refreshed.push(newName);
|
|
388
|
+
}
|
|
381
389
|
for (const fullPath of found) {
|
|
382
390
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
383
391
|
log(` ✕ ${path.basename(fullPath)}/ (renamed to ${newName})`, 'dim');
|
|
384
392
|
}
|
|
385
393
|
}
|
|
394
|
+
return refreshed;
|
|
386
395
|
}
|
|
387
396
|
|
|
388
397
|
function copyRecursive(src, dest, baseDir) {
|
|
@@ -410,6 +419,110 @@ function clearInstallMarker(skillsDir) {
|
|
|
410
419
|
fs.rmSync(path.join(skillsDir, DEVLYN_INSTALL_MARKER), { force: true });
|
|
411
420
|
}
|
|
412
421
|
|
|
422
|
+
function readInstallManifest(root) {
|
|
423
|
+
const markerPath = path.join(root, DEVLYN_INSTALL_MARKER);
|
|
424
|
+
const stat = fs.lstatSync(markerPath, { throwIfNoEntry: false });
|
|
425
|
+
if (!stat) return {};
|
|
426
|
+
if (!stat.isFile()) throw new Error(`not a regular install marker: ${markerPath}`);
|
|
427
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
428
|
+
if (marker?.package !== PKG.name || marker.schemaVersion !== 1) {
|
|
429
|
+
throw new Error('not a devlyn-cli install marker');
|
|
430
|
+
}
|
|
431
|
+
return marker.skills || {};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Enumerate every directory this install may replace, migrate or remove, before
|
|
435
|
+
// touching any target. Optional addons must be selected before this preflight too.
|
|
436
|
+
function plannedSkills(root, optionalNames = []) {
|
|
437
|
+
const names = new Set([...DEVLYN_CORE_SKILLS, ...optionalNames]);
|
|
438
|
+
for (const relative of DEPRECATED_DIRS) {
|
|
439
|
+
for (const full of legacySkillPaths(root, path.basename(relative))) names.add(path.basename(full));
|
|
440
|
+
}
|
|
441
|
+
for (const [oldName, newName] of Object.entries(RENAMED_SKILLS)) {
|
|
442
|
+
const oldPaths = legacySkillPaths(root, oldName);
|
|
443
|
+
oldPaths.forEach((full) => names.add(path.basename(full)));
|
|
444
|
+
if (oldPaths.some((full) => fs.lstatSync(full, { throwIfNoEntry: false }))
|
|
445
|
+
|| isShippedCopy(path.join(root, newName), PRE_STANDARD_SKILL_MD_SHA256)) names.add(newName);
|
|
446
|
+
}
|
|
447
|
+
for (const [name, hashes] of Object.entries(RETIRED_SKILL_MD_SHA256)) {
|
|
448
|
+
if (isShippedCopy(path.join(root, name), hashes)) names.add(name);
|
|
449
|
+
}
|
|
450
|
+
return names;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// `commandDir`: the project .claude/ whose DEPRECATED_FILES this install deletes, or null.
|
|
454
|
+
function preflightSkills(roots, optionalNames = [], commandDir = null) {
|
|
455
|
+
const conflicts = [];
|
|
456
|
+
try {
|
|
457
|
+
// Do not follow a commands link into another installation either.
|
|
458
|
+
const commands = commandDir && fs.lstatSync(path.join(commandDir, 'commands'), { throwIfNoEntry: false });
|
|
459
|
+
if (commands && !commands.isDirectory()) throw new Error('not a real directory');
|
|
460
|
+
const tracked = commandDir ? trackedPaths(path.join(commandDir, 'commands')) : [];
|
|
461
|
+
for (const relPath of commandDir ? DEPRECATED_FILES : []) {
|
|
462
|
+
const fullPath = path.join(commandDir, relPath);
|
|
463
|
+
const stat = fs.lstatSync(fullPath, { throwIfNoEntry: false });
|
|
464
|
+
if (!stat || (stat.isFile() && SKILL_HISTORY.commands[relPath].includes(contentHash(fullPath)))) continue;
|
|
465
|
+
const isTracked = tracked.some((file) => file.toLowerCase() === fullPath.toLowerCase());
|
|
466
|
+
conflicts.push(`${fullPath}: ${isTracked ? 'git-tracked; ' : ''}`
|
|
467
|
+
+ (stat.isFile() ? 'fingerprint mismatch with known shipped copies' : 'link or special file'));
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
conflicts.push(`${path.join(commandDir, 'commands')}: ${error.message}`);
|
|
471
|
+
}
|
|
472
|
+
for (const root of roots) {
|
|
473
|
+
try {
|
|
474
|
+
// Do not follow an agent-directory or skill-root link into another installation.
|
|
475
|
+
for (const dir of [path.dirname(root), root]) {
|
|
476
|
+
const stat = fs.lstatSync(dir, { throwIfNoEntry: false });
|
|
477
|
+
if (stat && !stat.isDirectory()) throw new Error(`not a real directory: ${dir}`);
|
|
478
|
+
}
|
|
479
|
+
const tracked = trackedPaths(root);
|
|
480
|
+
let manifest;
|
|
481
|
+
try {
|
|
482
|
+
manifest = readInstallManifest(root);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
conflicts.push(`${path.join(root, DEVLYN_INSTALL_MARKER)}: ${error.message}`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
for (const name of plannedSkills(root, optionalNames)) {
|
|
488
|
+
const dest = path.join(root, name);
|
|
489
|
+
const isTracked = tracked.some((file) => file.toLowerCase() === dest.toLowerCase()
|
|
490
|
+
|| file.toLowerCase().startsWith(dest.toLowerCase() + path.sep));
|
|
491
|
+
try {
|
|
492
|
+
if (!fs.lstatSync(dest, { throwIfNoEntry: false })) {
|
|
493
|
+
if (isTracked) throw new Error('missing contents; cannot verify installer ownership');
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const actual = fingerprint(dest);
|
|
497
|
+
const logicalName = name.replace(/[:\uF03A]/g, '-');
|
|
498
|
+
const source = path.join(DEVLYN_CORE_SKILLS.includes(logicalName)
|
|
499
|
+
? path.join(CONFIG_SOURCE, 'skills') : OPTIONAL_SKILLS_SOURCE, logicalName);
|
|
500
|
+
const current = fs.existsSync(source) && fingerprint(source) === actual;
|
|
501
|
+
const historical = SKILL_HISTORY.skills[logicalName]?.includes(actual);
|
|
502
|
+
const singleFile = isShippedCopy(dest, RETIRED_SKILL_MD_SHA256[name] || PRE_STANDARD_SKILL_MD_SHA256);
|
|
503
|
+
if (!current && !historical && !singleFile && manifest[name] !== actual) {
|
|
504
|
+
throw new Error(manifest[name] ? 'modified contents (install manifest fingerprint mismatch)'
|
|
505
|
+
: fs.existsSync(source) || SKILL_HISTORY.skills[logicalName]?.length
|
|
506
|
+
? 'fingerprint mismatch with known shipped copies'
|
|
507
|
+
: 'unknown installer ownership (no fingerprint history for this copy)');
|
|
508
|
+
}
|
|
509
|
+
} catch (error) {
|
|
510
|
+
conflicts.push(`${dest}: ${isTracked ? 'git-tracked; ' : ''}${error.message}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} catch (error) {
|
|
514
|
+
conflicts.push(`${root}: ${error.message}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (conflicts.length) {
|
|
518
|
+
throw new InstructionError('Installation refused; nothing was changed:\n'
|
|
519
|
+
+ conflicts.map((conflict) => ` - ${conflict}`).join('\n')
|
|
520
|
+
+ '\nMove these out of the install path, or back them up and remove them, then rerun.'
|
|
521
|
+
+ '\nFor tracked paths, also remove them from the Git index.'
|
|
522
|
+
+ '\nRerun the same command after resolving every conflict; -y does not bypass this check.');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
413
526
|
function assertCompleteSkillInstall(sourceSkillsDir, skillsDir, skillNames) {
|
|
414
527
|
function complete(src, dest) {
|
|
415
528
|
if (!fs.existsSync(src) || !fs.existsSync(dest)) return false;
|
|
@@ -429,161 +542,87 @@ function assertCompleteSkillInstall(sourceSkillsDir, skillsDir, skillNames) {
|
|
|
429
542
|
}
|
|
430
543
|
}
|
|
431
544
|
|
|
432
|
-
function writeInstallMarker(skillsDir) {
|
|
545
|
+
function writeInstallMarker(skillsDir, names, previous) {
|
|
433
546
|
const markerPath = path.join(skillsDir, DEVLYN_INSTALL_MARKER);
|
|
434
547
|
const tempPath = `${markerPath}.${process.pid}.tmp`;
|
|
435
548
|
const marker = {
|
|
436
549
|
schemaVersion: 1,
|
|
437
550
|
package: PKG.name,
|
|
438
551
|
version: PKG.version,
|
|
552
|
+
skills: { ...previous, ...Object.fromEntries(names.map((name) => [name, fingerprint(path.join(skillsDir, name))])) },
|
|
439
553
|
};
|
|
554
|
+
const fd = fs.openSync(tempPath, 'wx', 0o600);
|
|
440
555
|
try {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
556
|
+
try {
|
|
557
|
+
fs.writeFileSync(fd, JSON.stringify(marker, null, 2) + '\n', 'utf8');
|
|
558
|
+
} finally {
|
|
559
|
+
fs.closeSync(fd);
|
|
560
|
+
}
|
|
446
561
|
fs.renameSync(tempPath, markerPath);
|
|
447
562
|
} finally {
|
|
448
563
|
fs.rmSync(tempPath, { force: true });
|
|
449
564
|
}
|
|
450
565
|
}
|
|
451
566
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
console.log(` ${COLORS.dim}${item.desc}${COLORS.reset}`);
|
|
477
|
-
});
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
render();
|
|
481
|
-
|
|
482
|
-
process.stdin.setRawMode(true);
|
|
483
|
-
process.stdin.resume();
|
|
484
|
-
process.stdin.setEncoding('utf8');
|
|
485
|
-
|
|
486
|
-
const onKeypress = (key) => {
|
|
487
|
-
// Ctrl+C
|
|
488
|
-
if (key === '\u0003') {
|
|
489
|
-
process.stdin.setRawMode(false);
|
|
490
|
-
process.stdin.removeListener('data', onKeypress);
|
|
491
|
-
process.exit();
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Enter
|
|
495
|
-
if (key === '\r' || key === '\n') {
|
|
496
|
-
process.stdin.setRawMode(false);
|
|
497
|
-
process.stdin.removeListener('data', onKeypress);
|
|
498
|
-
process.stdin.pause();
|
|
499
|
-
console.log('');
|
|
500
|
-
resolve([...selected].map((i) => items[i]));
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Space - toggle selection
|
|
505
|
-
if (key === ' ') {
|
|
506
|
-
if (selected.has(cursor)) {
|
|
507
|
-
selected.delete(cursor);
|
|
508
|
-
} else {
|
|
509
|
-
selected.add(cursor);
|
|
510
|
-
}
|
|
511
|
-
render();
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Arrow up or k
|
|
516
|
-
if (key === '\x1b[A' || key === 'k') {
|
|
517
|
-
cursor = cursor > 0 ? cursor - 1 : items.length - 1;
|
|
518
|
-
render();
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Arrow down or j
|
|
523
|
-
if (key === '\x1b[B' || key === 'j') {
|
|
524
|
-
cursor = cursor < items.length - 1 ? cursor + 1 : 0;
|
|
525
|
-
render();
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// 'a' - select all
|
|
530
|
-
if (key === 'a') {
|
|
531
|
-
if (selected.size === items.length) {
|
|
532
|
-
selected.clear();
|
|
533
|
-
} else {
|
|
534
|
-
items.forEach((_, i) => selected.add(i));
|
|
535
|
-
}
|
|
536
|
-
render();
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
};
|
|
567
|
+
// One Inquirer menu, imported only when shown (Inquirer is ESM). The page gets the terminal
|
|
568
|
+
// rows left after everything else the menu draws, so a redraw never scrolls it into
|
|
569
|
+
// duplicates. Ctrl+C cancels the install.
|
|
570
|
+
async function ask(module, help, config) {
|
|
571
|
+
const { default: prompt } = await import(module);
|
|
572
|
+
const { stdin: input, stdout: output } = process;
|
|
573
|
+
const columns = output.columns || 80;
|
|
574
|
+
// Most rows a text wraps to: an ASCII character takes one cell, any other at most two.
|
|
575
|
+
const rows = (text = '') => Math.ceil([...text].reduce((cells, c) => cells + (c.codePointAt(0) < 0x80 ? 1 : 2), 0) / columns);
|
|
576
|
+
// Message, blank line, the active choice's description, key help, and the line Inquirer
|
|
577
|
+
// adds when the help exactly fills the width.
|
|
578
|
+
const reserved = rows(`? ${config.message}`) + 1 + Math.max(...config.choices.map((choice) => rows(choice.description))) + rows(help) + 1;
|
|
579
|
+
try {
|
|
580
|
+
return await prompt({
|
|
581
|
+
...config,
|
|
582
|
+
pageSize: Math.max(1, (output.rows || 24) - reserved),
|
|
583
|
+
theme: { keybindings: ['vim'], style: { keysHelpTip: () => help } },
|
|
584
|
+
}, { input, output });
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (error.name !== 'ExitPromptError') throw error;
|
|
587
|
+
console.log('Installation cancelled.');
|
|
588
|
+
process.exit(0);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
540
591
|
|
|
541
|
-
|
|
592
|
+
// Checkbox menu (space toggles, a toggles all); resolves to the chosen items.
|
|
593
|
+
function multiSelect(message, items, preselectedIndices = []) {
|
|
594
|
+
return ask('@inquirer/checkbox', '↑↓ navigate · space select · a all · enter confirm', {
|
|
595
|
+
message,
|
|
596
|
+
choices: items.map((item, index) => ({
|
|
597
|
+
name: item.type ? `${item.name} [${item.type === 'mcp' ? 'mcp' : item.type === 'local' ? 'skill' : 'pack'}]` : item.name,
|
|
598
|
+
short: item.name,
|
|
599
|
+
description: item.desc,
|
|
600
|
+
value: item,
|
|
601
|
+
checked: preselectedIndices.includes(index),
|
|
602
|
+
})),
|
|
542
603
|
});
|
|
543
604
|
}
|
|
544
605
|
|
|
545
|
-
// One of `items
|
|
546
|
-
function singleSelect(items, initial) {
|
|
547
|
-
return
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (drawn) process.stdout.write(`\x1b[${items.length + 2}A\x1b[0J`);
|
|
552
|
-
drawn = true;
|
|
553
|
-
console.log(`${COLORS.dim}(↑↓ navigate, enter confirm)${COLORS.reset}\n`);
|
|
554
|
-
items.forEach((item, i) => console.log(i === cursor ? `${COLORS.cyan}❯ ${item}${COLORS.reset}` : ` ${item}`));
|
|
555
|
-
};
|
|
556
|
-
render();
|
|
557
|
-
process.stdin.setRawMode(true);
|
|
558
|
-
process.stdin.resume();
|
|
559
|
-
process.stdin.setEncoding('utf8');
|
|
560
|
-
const onKeypress = (key) => {
|
|
561
|
-
if (key === '\u0003') {
|
|
562
|
-
process.stdin.setRawMode(false);
|
|
563
|
-
process.exit();
|
|
564
|
-
}
|
|
565
|
-
if (key === '\r' || key === '\n') {
|
|
566
|
-
process.stdin.setRawMode(false);
|
|
567
|
-
process.stdin.removeListener('data', onKeypress);
|
|
568
|
-
process.stdin.pause();
|
|
569
|
-
console.log('');
|
|
570
|
-
resolve(cursor);
|
|
571
|
-
} else if (['\x1b[A', 'k', '\x1b[B', 'j'].includes(key)) {
|
|
572
|
-
cursor = (cursor + (key === '\x1b[A' || key === 'k' ? items.length - 1 : 1)) % items.length;
|
|
573
|
-
render();
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
process.stdin.on('data', onKeypress);
|
|
606
|
+
// One of `items`; resolves to the chosen index.
|
|
607
|
+
function singleSelect(message, items, initial) {
|
|
608
|
+
return ask('@inquirer/select', '↑↓ navigate · enter confirm', {
|
|
609
|
+
message,
|
|
610
|
+
choices: items.map((name, index) => ({ name, value: index })),
|
|
611
|
+
default: initial,
|
|
577
612
|
});
|
|
578
613
|
}
|
|
579
614
|
|
|
615
|
+
// After install(), whose preflight already cleared every selected addon before any change.
|
|
580
616
|
function installLocalSkill(skillName, roots) {
|
|
581
617
|
if (!fs.existsSync(path.join(OPTIONAL_SKILLS_SOURCE, skillName))) {
|
|
582
618
|
log(` ⚠️ Skill "${skillName}" not found`, 'yellow');
|
|
583
619
|
return false;
|
|
584
620
|
}
|
|
585
621
|
log(`\n🛠️ Installing ${skillName}...`, 'cyan');
|
|
586
|
-
for (const root of roots)
|
|
622
|
+
for (const root of roots) {
|
|
623
|
+
installOptionalSkillInto(root, skillName);
|
|
624
|
+
writeInstallMarker(root, [skillName], readInstallManifest(root));
|
|
625
|
+
}
|
|
587
626
|
return true;
|
|
588
627
|
}
|
|
589
628
|
|
|
@@ -653,6 +692,7 @@ function installCoreSkills(skillsDir) {
|
|
|
653
692
|
const sourceSkillsDir = path.join(CONFIG_SOURCE, 'skills');
|
|
654
693
|
log(`\n📁 Installing devlyn skills to ${skillsDir.replace(os.homedir(), '~')}`, 'green');
|
|
655
694
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
695
|
+
const previous = readInstallManifest(skillsDir);
|
|
656
696
|
clearInstallMarker(skillsDir);
|
|
657
697
|
const removed = cleanupDeprecated(skillsDir);
|
|
658
698
|
if (removed > 0) {
|
|
@@ -667,8 +707,8 @@ function installCoreSkills(skillsDir) {
|
|
|
667
707
|
copyRecursive(src, dest, skillsDir);
|
|
668
708
|
}
|
|
669
709
|
assertCompleteSkillInstall(sourceSkillsDir, skillsDir, DEVLYN_CORE_SKILLS);
|
|
670
|
-
retireRenamedSkills(skillsDir);
|
|
671
|
-
writeInstallMarker(skillsDir);
|
|
710
|
+
const refreshed = retireRenamedSkills(skillsDir);
|
|
711
|
+
writeInstallMarker(skillsDir, [...DEVLYN_CORE_SKILLS, ...refreshed], previous);
|
|
672
712
|
}
|
|
673
713
|
|
|
674
714
|
// Keep installer-managed pipeline state and install metadata out of git.
|
|
@@ -820,7 +860,7 @@ function installClaudeCore() {
|
|
|
820
860
|
}
|
|
821
861
|
|
|
822
862
|
// Installs the targets in one scope; returns the skill roots written.
|
|
823
|
-
function install(targets, global) {
|
|
863
|
+
function install(targets, global, optionalNames = []) {
|
|
824
864
|
// In the home folder CLAUDE.md, AGENTS.md (agents read a parent folder's too) and
|
|
825
865
|
// .claude/settings.json would apply to every project. Same folder by identity, so a link or
|
|
826
866
|
// another spelling of the path is caught too.
|
|
@@ -831,6 +871,8 @@ function install(targets, global) {
|
|
|
831
871
|
+ 'would apply to every project. Run from a project folder, or use --global for skills only.');
|
|
832
872
|
}
|
|
833
873
|
const roots = targets.flatMap((target) => skillRoots(target, global));
|
|
874
|
+
preflightSkills(roots, optionalNames,
|
|
875
|
+
!global && targets.includes('claude') ? path.dirname(skillRoots('claude', false)[0]) : null);
|
|
834
876
|
// AGENTS.md first, whatever order the menu gave: a new CLAUDE.md holds the block only when AGENTS.md does not.
|
|
835
877
|
for (const target of ['agents', 'claude'].filter((name) => targets.includes(name))) {
|
|
836
878
|
if (global) skillRoots(target, true).forEach((root) => installCoreSkills(root));
|
|
@@ -885,13 +927,12 @@ async function init({ yes, claude, global }) {
|
|
|
885
927
|
return;
|
|
886
928
|
}
|
|
887
929
|
|
|
888
|
-
log('\n🎯 What to install:\n', 'blue');
|
|
889
930
|
const targetOptions = [
|
|
890
931
|
{ key: 'agents', name: 'AGENTS.md — Codex · omp · Pi · Grok', desc: 'AGENTS.md + .agents/skills' },
|
|
891
932
|
{ key: 'claude', name: 'CLAUDE.md — Claude Code', desc: 'CLAUDE.md + .claude/ (skills, settings)' },
|
|
892
933
|
];
|
|
893
934
|
const preselected = claude || hasDevlynClaude(false) || (global && hasDevlynClaude(true)) ? [0, 1] : [0];
|
|
894
|
-
const targets = (await multiSelect(targetOptions, preselected)).map((option) => option.key);
|
|
935
|
+
const targets = (await multiSelect('What to install', targetOptions, preselected)).map((option) => option.key);
|
|
895
936
|
|
|
896
937
|
if (targets.length === 0) {
|
|
897
938
|
log('\n💡 Nothing selected — nothing installed.', 'yellow');
|
|
@@ -899,14 +940,10 @@ async function init({ yes, claude, global }) {
|
|
|
899
940
|
return;
|
|
900
941
|
}
|
|
901
942
|
|
|
902
|
-
|
|
903
|
-
const scope = await singleSelect(['This project', 'Global — every project on this machine'], global ? 1 : 0);
|
|
904
|
-
const roots = install(targets, scope === 1);
|
|
905
|
-
|
|
943
|
+
const scope = await singleSelect('Where', ['This project', 'Global — every project on this machine'], global ? 1 : 0);
|
|
906
944
|
// Ask about optional addons (local skills + external packs; MCP servers belong to Claude Code)
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const selectedAddons = await multiSelect(OPTIONAL_ADDONS.filter((addon) => addon.type !== 'mcp' || targets.includes('claude')));
|
|
945
|
+
const selectedAddons = await multiSelect('Optional skills & packs', OPTIONAL_ADDONS.filter((addon) => addon.type !== 'mcp' || targets.includes('claude')));
|
|
946
|
+
const roots = install(targets, scope === 1, selectedAddons.filter((addon) => addon.type === 'local').map((addon) => addon.name));
|
|
910
947
|
|
|
911
948
|
if (selectedAddons.length > 0) {
|
|
912
949
|
for (const addon of selectedAddons) {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": "devlyn-cli@4.1.0 (npm tarball sha1 398ff07d3d689b56cc22e18ac50260bc6e4e9e85); devlyn-cli@4.2.0 (npm tarball sha1 c93c2fb1cf5630b18d2eba0085182c14fddbe2d5); whole-tree fingerprints from bin/skill-ownership.js; deprecated commands: commands/ and optional-commands/*/ files of devlyn-cli 0.0.1-0.5.8 (every published version that shipped them), LF-normalized SHA-256",
|
|
3
|
+
"skills": {
|
|
4
|
+
"_shared": [
|
|
5
|
+
"eb3327d5e6940bd5e0459fa28c9ab323d9c58ccfdeb9426c19cd027cc4e5c4ed",
|
|
6
|
+
"8c80926ab03b2477e52264495b35c2116f65fc51b02e8c56e441b5705872392a"
|
|
7
|
+
],
|
|
8
|
+
"devlyn-design-ui": [
|
|
9
|
+
"91080266232fb62b6f1d45932184f065589e12eb66f02fb6442527c6dfcd81b6"
|
|
10
|
+
],
|
|
11
|
+
"devlyn-engines": [
|
|
12
|
+
"3451d37f47e6e707cff92baa12d04b43aee94301456b8e280c09332e598a1c2c",
|
|
13
|
+
"89f0efaa2efdfddc887b494138247980d97856f6cabe6a8dcc585b3a28e5fa88"
|
|
14
|
+
],
|
|
15
|
+
"devlyn-ideate": [
|
|
16
|
+
"8ff5148cc65b4608c35b29afd6fef20d42d8da93a094144d98cfc63e74089b6e",
|
|
17
|
+
"5824f07e455fe1c3942bf595a12e935258d4c0a851ca1bf741db915603d1c4e0"
|
|
18
|
+
],
|
|
19
|
+
"devlyn-queue": [
|
|
20
|
+
"4c7d988dcd154c7bf658d2d24cd7868955df9d854ed87eb784742d6d4bb229e0"
|
|
21
|
+
],
|
|
22
|
+
"devlyn-resolve": [
|
|
23
|
+
"2abd303851f085da07f6b55ee1724842ef7f8e8ad6b68c6339df5f1eaa8c69de"
|
|
24
|
+
],
|
|
25
|
+
"asset-creator": [
|
|
26
|
+
"e295f57eca534cbd4e27e236999f4f5412ebdc6944ac864761e922dea213f904"
|
|
27
|
+
],
|
|
28
|
+
"better-auth-setup": [
|
|
29
|
+
"e97c39c5d669ffc1f49d57fd3a016e852727e57c3abe2f9be1ebffab9fcb19ec"
|
|
30
|
+
],
|
|
31
|
+
"cloudflare-nextjs-setup": [
|
|
32
|
+
"f147a28b9f6f62feb21df89bdf08869820f4871ede7a8ba4a242246509ee6e35"
|
|
33
|
+
],
|
|
34
|
+
"code-health-standards": [
|
|
35
|
+
"2056617b324bb7e1e3b85a6536bfe33151e31ef421656cade74258fb4e922c1b"
|
|
36
|
+
],
|
|
37
|
+
"code-review-standards": [
|
|
38
|
+
"3fe0742825aff05fa6fda49d3e3e0fbf5457307af944eb90f2771cbf611edd69"
|
|
39
|
+
],
|
|
40
|
+
"devlyn-pencil-pull": [
|
|
41
|
+
"65af00f88b0837ffd377ebf363da73c766ae0ae8e20716fe18f80edfa91bd6da"
|
|
42
|
+
],
|
|
43
|
+
"devlyn-pencil-push": [
|
|
44
|
+
"76955458ced72f4b0bdaf71482be6097c4c86876934b6e6a2ac7d19f850662ad"
|
|
45
|
+
],
|
|
46
|
+
"devlyn-reap": [
|
|
47
|
+
"77e029ab06c37fb7135a9bd4395ff84b84ab2e44cf79c5827798777e39472351"
|
|
48
|
+
],
|
|
49
|
+
"dokkit": [
|
|
50
|
+
"42045db4e2f70f68d0ed90a9c7c5c2660a6d5183ea641dfa9f2f1f0d0938e3fb"
|
|
51
|
+
],
|
|
52
|
+
"generate-skill": [
|
|
53
|
+
"16a78b867539c0bc11a44f3ecfa1762ba7fcba8841fca8558abe818fc961ff68"
|
|
54
|
+
],
|
|
55
|
+
"polar-billing-setup": [
|
|
56
|
+
"8b6dc842903e7343ac7b1e3a1c55911a9987552196ffb27a11a86093537fe976"
|
|
57
|
+
],
|
|
58
|
+
"prompt-engineering": [
|
|
59
|
+
"d7a0718ab0fd7e0a3c0d8ef8bc4e0349edfc843648343f08f9776e1ea047573c"
|
|
60
|
+
],
|
|
61
|
+
"pyx-scan": [
|
|
62
|
+
"60f02fa049616e94fff4318c96dc9f417b3def7ec75a3bd0846f2bcbd99b0b69"
|
|
63
|
+
],
|
|
64
|
+
"root-cause-analysis": [
|
|
65
|
+
"eeb20fe09edd37b07360c3440b68bdb488abee4917d4e808e64e3889c6addf5a"
|
|
66
|
+
],
|
|
67
|
+
"ui-implementation-standards": [
|
|
68
|
+
"0fbaad2b7d77462916d9364b97b921354ffbf945a68e9e0d422b3a88d36d2569"
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
"commands": {
|
|
72
|
+
"commands/devlyn.handoff.md": [
|
|
73
|
+
"91d143c6305e95e4af6992eda0d127e94a394c2e91a2d409b4d41ca03c23f470"
|
|
74
|
+
],
|
|
75
|
+
"commands/devlyn.clean.md": [
|
|
76
|
+
"918c6ae867c57a4d9748237adae0df8c78a1447c7c028a0af988b59a5fbb0d9a"
|
|
77
|
+
],
|
|
78
|
+
"commands/devlyn.design-system.md": [
|
|
79
|
+
"f81166b5ba08d9ee1f5cff33c911ea387e5cb50141a9712a96fc4434c60a673f"
|
|
80
|
+
],
|
|
81
|
+
"commands/devlyn.design-ui.md": [
|
|
82
|
+
"0496a6b2dd8c6bb0474b71ef9a319a6b95bb76972d0f07a01c33f8860fe83dfb",
|
|
83
|
+
"4e2aafcc0de6ce7f3022a353aaa9fb4330e76a50a91ab970b9b9a0f192f9f1f2"
|
|
84
|
+
],
|
|
85
|
+
"commands/devlyn.discover-product.md": [
|
|
86
|
+
"a609401d29cd2bdad7a48c7c8f096b442fe03a17c6b2c3151936c47b4a16e6c6",
|
|
87
|
+
"1e66aa35bf300ec6167e1b888f43a92ebd9e60d501397ff0334c9a7809d433f5"
|
|
88
|
+
],
|
|
89
|
+
"commands/devlyn.evaluate.md": [
|
|
90
|
+
"8f17ccc639d54adab24c49dcf471376bab97511675d00554056a4c1f67a14115"
|
|
91
|
+
],
|
|
92
|
+
"commands/devlyn.feature-spec.md": [
|
|
93
|
+
"d442ed7ae8d14a255c37f1b120b758f69794be88e2970a8a0c0e51519344b6c7"
|
|
94
|
+
],
|
|
95
|
+
"commands/devlyn.implement-ui.md": [
|
|
96
|
+
"a8d0813781b2f7a3df5a4751c92755bf7c84c53428cf63cea34c965939b8c478"
|
|
97
|
+
],
|
|
98
|
+
"commands/devlyn.product-spec.md": [
|
|
99
|
+
"7a92d2df2586b44e2c4d9182eeb3d61f7adc2a13bed4d2cd26abcfe086585895"
|
|
100
|
+
],
|
|
101
|
+
"commands/devlyn.recommend-features.md": [
|
|
102
|
+
"2b92523017e1da10c299dafb19d84736de5084b5b46693de7fd2d6df51dc56b4"
|
|
103
|
+
],
|
|
104
|
+
"commands/devlyn.resolve.md": [
|
|
105
|
+
"48c0c3ee553f87f1f0733190effc7220c01c82837a5300ef68c9ffdf30c5d3a1",
|
|
106
|
+
"24595766e1a81a1fcacb78261b03c3665bc686bcac8b1805d1c4db379584879f",
|
|
107
|
+
"eaad5354cfc20ed378ffd2a2e1ca6bade1310bd2a47ab93e5942e9599c3ba9a8"
|
|
108
|
+
],
|
|
109
|
+
"commands/devlyn.review.md": [
|
|
110
|
+
"55888e08bf31ead1d359f218277cfa7efc3c6cb0f96fadc665d5a2406ff115d3",
|
|
111
|
+
"22f532f587f6d2fbf2662e38d5073b85a030141573f86d92ad40b9d292836fb7",
|
|
112
|
+
"7f9ce1db4ffd3cdaf9fd949e4b07de04e0931c91cd846d4ab220ff6114a45b17",
|
|
113
|
+
"3a511fc678e599ff4b4c37fd9191399a599e1ba9ea9c40fc4afcbe29cb188d2b"
|
|
114
|
+
],
|
|
115
|
+
"commands/devlyn.team-design-ui.md": [
|
|
116
|
+
"02485911588e9f52afb5a5082fd7804ee7a41bad44323960a2afe1a16a49d628"
|
|
117
|
+
],
|
|
118
|
+
"commands/devlyn.team-resolve.md": [
|
|
119
|
+
"8f11fa0c3203c0dd5bf09b5243dcff5968a36275682c54b3a012262fc545859a",
|
|
120
|
+
"09fc163d34dfe8906dd8d22501a028bfbbad639499acab01e5e60f06fc520382",
|
|
121
|
+
"ccfc5083813ee50d407109616ccbc2509aa7526316cebbf7a271537abdfff3b7",
|
|
122
|
+
"72f66be3c4c27a46e76d56d13a0d381880c054a03243188f98da2faf6dc77295",
|
|
123
|
+
"54c52c3f27998998ed55cc648d071bbb10444761b819c59ed118b795596a4aee"
|
|
124
|
+
],
|
|
125
|
+
"commands/devlyn.team-review.md": [
|
|
126
|
+
"58b36cfe2749968d697d486a104a1b139e4312730b0ac2933550634f6ece48db",
|
|
127
|
+
"7e5c2d93a23d51af7939dae29c70095dbd8a4a681cc06d4d578f2de29df346a3",
|
|
128
|
+
"c8e145955a303141af4082dae174e3b3f1681e4c9698e25ae337eadd31fb883b"
|
|
129
|
+
],
|
|
130
|
+
"commands/devlyn.update-docs.md": [
|
|
131
|
+
"86945afccf24762a88d26eef3703fbf6c5a90a3343fd3de72a9cb26b6f62d3b5"
|
|
132
|
+
],
|
|
133
|
+
"commands/devlyn.pencil-pull.md": [
|
|
134
|
+
"dfdd3d19ca676558bfa2bb3ce398181b0a081ab88d1feaf69c54c706a9671631"
|
|
135
|
+
],
|
|
136
|
+
"commands/devlyn.pencil-push.md": [
|
|
137
|
+
"5f2e8b29609cbb2af72941579823bec598265ebf29cd3a375d6aa47d95b74354"
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
}
|