destinotes-skills 0.2.0 → 0.3.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.
- package/README.md +1 -1
- package/bin/install.mjs +2 -0
- package/bin/lib/config.mjs +23 -4
- package/bin/lib/discover.mjs +88 -0
- package/bin/lib/migrate.mjs +95 -0
- package/bin/lib/prompt.mjs +28 -0
- package/bin/setup.mjs +110 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ This runs an **interactive setup** that walks you through:
|
|
|
19
19
|
- Which agents to install to (Cursor, Claude Code)
|
|
20
20
|
- Project-level vs global skill install
|
|
21
21
|
|
|
22
|
-
Settings are saved to `./destinotes/destinotes.config.json`.
|
|
22
|
+
Settings are saved to `./destinotes/destinotes.config.json`. On first run, setup **scans for existing `destinotes` folders** (root, `docs/`, `wiki/`, etc.). If one is found, you can confirm it and optionally **move everything to `./destinotes/`**.
|
|
23
23
|
|
|
24
24
|
### Non-interactive / CI
|
|
25
25
|
|
package/bin/install.mjs
CHANGED
|
@@ -87,6 +87,8 @@ Options:
|
|
|
87
87
|
-h, --help Show this help
|
|
88
88
|
|
|
89
89
|
Interactive setup configures:
|
|
90
|
+
- Existing destinotes folder discovery (root, docs/, wiki/, etc.)
|
|
91
|
+
- Optional migration to ./destinotes/
|
|
90
92
|
- Project name
|
|
91
93
|
- Repository URL (auto-detected from git remote when possible)
|
|
92
94
|
- Default branch
|
package/bin/lib/config.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
CANONICAL_DOCS_DIR,
|
|
5
|
+
discoverDestinotesFolders,
|
|
6
|
+
findConfigInDiscoveredFolders,
|
|
7
|
+
} from "./discover.mjs";
|
|
3
8
|
|
|
4
9
|
export const CONFIG_VERSION = 1;
|
|
5
10
|
export const CONFIG_FILENAME = "destinotes.config.json";
|
|
@@ -8,14 +13,28 @@ export function getConfigPath(cwd, docsDir) {
|
|
|
8
13
|
return join(cwd, docsDir, CONFIG_FILENAME);
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
export function readConfig(cwd, docsDir
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
export function readConfig(cwd, docsDir) {
|
|
17
|
+
if (docsDir) {
|
|
18
|
+
const configPath = getConfigPath(cwd, docsDir);
|
|
19
|
+
if (!existsSync(configPath)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(configPath, "utf8"));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const located = findConfigInDiscoveredFolders(cwd);
|
|
31
|
+
if (!located) {
|
|
14
32
|
return null;
|
|
15
33
|
}
|
|
16
34
|
|
|
17
35
|
try {
|
|
18
|
-
|
|
36
|
+
const config = JSON.parse(readFileSync(located.configPath, "utf8"));
|
|
37
|
+
return { ...config, docsDir: config.docsDir ?? located.folder };
|
|
19
38
|
} catch {
|
|
20
39
|
return null;
|
|
21
40
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const CANONICAL_DOCS_DIR = "destinotes";
|
|
5
|
+
|
|
6
|
+
/** Relative paths (from project root) where a `destinotes` folder is commonly placed. */
|
|
7
|
+
const COMMON_DESTINOTES_PATHS = [
|
|
8
|
+
"destinotes",
|
|
9
|
+
"docs/destinotes",
|
|
10
|
+
"doc/destinotes",
|
|
11
|
+
"documentation/destinotes",
|
|
12
|
+
"wiki/destinotes",
|
|
13
|
+
"content/destinotes",
|
|
14
|
+
"knowledge/destinotes",
|
|
15
|
+
"notes/destinotes",
|
|
16
|
+
"internal/destinotes",
|
|
17
|
+
".destinotes",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function countEntries(dirPath) {
|
|
21
|
+
let files = 0;
|
|
22
|
+
let dirs = 0;
|
|
23
|
+
|
|
24
|
+
for (const entry of readdirSync(dirPath)) {
|
|
25
|
+
const fullPath = join(dirPath, entry);
|
|
26
|
+
if (statSync(fullPath).isDirectory()) {
|
|
27
|
+
dirs += 1;
|
|
28
|
+
} else {
|
|
29
|
+
files += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { files, dirs, total: files + dirs };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function describeFolder(relativePath, counts) {
|
|
37
|
+
const parts = [];
|
|
38
|
+
if (counts.files > 0) {
|
|
39
|
+
parts.push(`${counts.files} file${counts.files === 1 ? "" : "s"}`);
|
|
40
|
+
}
|
|
41
|
+
if (counts.dirs > 0) {
|
|
42
|
+
parts.push(`${counts.dirs} folder${counts.dirs === 1 ? "" : "s"}`);
|
|
43
|
+
}
|
|
44
|
+
const summary = parts.length > 0 ? parts.join(", ") : "empty";
|
|
45
|
+
return { relative: relativePath, ...counts, summary };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function discoverDestinotesFolders(cwd) {
|
|
49
|
+
const found = [];
|
|
50
|
+
|
|
51
|
+
for (const relativePath of COMMON_DESTINOTES_PATHS) {
|
|
52
|
+
const absolutePath = join(cwd, relativePath);
|
|
53
|
+
if (!existsSync(absolutePath) || !statSync(absolutePath).isDirectory()) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
found.push(describeFolder(relativePath, countEntries(absolutePath)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return found.sort((a, b) => {
|
|
61
|
+
if (a.relative === CANONICAL_DOCS_DIR) {
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
if (b.relative === CANONICAL_DOCS_DIR) {
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
return a.relative.localeCompare(b.relative);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function findConfigInDiscoveredFolders(cwd, folders = discoverDestinotesFolders(cwd)) {
|
|
72
|
+
for (const folder of folders) {
|
|
73
|
+
const configPath = join(cwd, folder.relative, "destinotes.config.json");
|
|
74
|
+
if (existsSync(configPath)) {
|
|
75
|
+
return { folder: folder.relative, configPath };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isCanonicalDocsDir(relativePath) {
|
|
83
|
+
return relativePath === CANONICAL_DOCS_DIR;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function formatFolderOption(folder) {
|
|
87
|
+
return `./${folder.relative}/ (${folder.summary})`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { CANONICAL_DOCS_DIR } from "./discover.mjs";
|
|
12
|
+
|
|
13
|
+
function isEmptyDir(dirPath) {
|
|
14
|
+
return readdirSync(dirPath).length === 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function removeEmptyDirs(dirPath) {
|
|
18
|
+
if (!existsSync(dirPath) || !statSync(dirPath).isDirectory()) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const entry of readdirSync(dirPath)) {
|
|
23
|
+
const child = join(dirPath, entry);
|
|
24
|
+
if (statSync(child).isDirectory()) {
|
|
25
|
+
removeEmptyDirs(child);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (isEmptyDir(dirPath)) {
|
|
30
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function moveEntry(sourcePath, destPath) {
|
|
35
|
+
try {
|
|
36
|
+
renameSync(sourcePath, destPath);
|
|
37
|
+
} catch {
|
|
38
|
+
cpSync(sourcePath, destPath, { recursive: true, force: true });
|
|
39
|
+
rmSync(sourcePath, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function migrateToCanonical(cwd, sourceRelative) {
|
|
44
|
+
if (sourceRelative === CANONICAL_DOCS_DIR) {
|
|
45
|
+
return {
|
|
46
|
+
migrated: false,
|
|
47
|
+
source: sourceRelative,
|
|
48
|
+
dest: CANONICAL_DOCS_DIR,
|
|
49
|
+
moved: [],
|
|
50
|
+
skipped: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sourcePath = join(cwd, sourceRelative);
|
|
55
|
+
const destPath = join(cwd, CANONICAL_DOCS_DIR);
|
|
56
|
+
|
|
57
|
+
if (!existsSync(sourcePath)) {
|
|
58
|
+
throw new Error(`Source folder not found: ./${sourceRelative}/`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
mkdirSync(destPath, { recursive: true });
|
|
62
|
+
|
|
63
|
+
const moved = [];
|
|
64
|
+
const skipped = [];
|
|
65
|
+
|
|
66
|
+
for (const entry of readdirSync(sourcePath)) {
|
|
67
|
+
const from = join(sourcePath, entry);
|
|
68
|
+
const to = join(destPath, entry);
|
|
69
|
+
|
|
70
|
+
if (existsSync(to)) {
|
|
71
|
+
skipped.push(entry);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const stat = statSync(from);
|
|
76
|
+
if (stat.isDirectory()) {
|
|
77
|
+
cpSync(from, to, { recursive: true });
|
|
78
|
+
rmSync(from, { recursive: true, force: true });
|
|
79
|
+
} else {
|
|
80
|
+
moveEntry(from, to);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
moved.push(entry);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
removeEmptyDirs(sourcePath);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
migrated: true,
|
|
90
|
+
source: sourceRelative,
|
|
91
|
+
dest: CANONICAL_DOCS_DIR,
|
|
92
|
+
moved,
|
|
93
|
+
skipped,
|
|
94
|
+
};
|
|
95
|
+
}
|
package/bin/lib/prompt.mjs
CHANGED
|
@@ -22,6 +22,34 @@ export async function confirm(rl, question, defaultYes = true) {
|
|
|
22
22
|
return answer === "y" || answer === "yes";
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export async function chooseFromList(rl, prompt, options, { allowSkip = true } = {}) {
|
|
26
|
+
if (options.length === 0) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(`\n${prompt}`);
|
|
31
|
+
options.forEach((option, index) => {
|
|
32
|
+
console.log(` ${index + 1}. ${option.label}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (allowSkip) {
|
|
36
|
+
console.log(" (press Enter to skip)");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const answer = (await rl.question("Choice: ")).trim();
|
|
40
|
+
if (!answer) {
|
|
41
|
+
return allowSkip ? null : options[0]?.value ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
45
|
+
if (Number.isNaN(index) || index < 0 || index >= options.length) {
|
|
46
|
+
console.log("Invalid choice — skipping.");
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return options[index].value;
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
export async function chooseAgents(rl, detected, labels) {
|
|
26
54
|
const selected = [];
|
|
27
55
|
|
package/bin/setup.mjs
CHANGED
|
@@ -4,13 +4,96 @@ import {
|
|
|
4
4
|
readConfig,
|
|
5
5
|
scaffoldDocs,
|
|
6
6
|
} from "./lib/config.mjs";
|
|
7
|
+
import {
|
|
8
|
+
CANONICAL_DOCS_DIR,
|
|
9
|
+
discoverDestinotesFolders,
|
|
10
|
+
formatFolderOption,
|
|
11
|
+
isCanonicalDocsDir,
|
|
12
|
+
} from "./lib/discover.mjs";
|
|
13
|
+
import { migrateToCanonical } from "./lib/migrate.mjs";
|
|
7
14
|
import {
|
|
8
15
|
ask,
|
|
9
16
|
chooseAgents,
|
|
17
|
+
chooseFromList,
|
|
10
18
|
confirm,
|
|
11
19
|
createPrompter,
|
|
12
20
|
} from "./lib/prompt.mjs";
|
|
13
21
|
|
|
22
|
+
async function resolveDocsFolder(rl, cwd, existing) {
|
|
23
|
+
const discovered = discoverDestinotesFolders(cwd);
|
|
24
|
+
let docsDirDefault = existing?.docsDir ?? CANONICAL_DOCS_DIR;
|
|
25
|
+
let migrationResult = null;
|
|
26
|
+
|
|
27
|
+
if (discovered.length === 0) {
|
|
28
|
+
return { docsDirDefault, migrationResult };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log("\nExisting Destinotes folders found:");
|
|
32
|
+
|
|
33
|
+
let selectedRelative = null;
|
|
34
|
+
|
|
35
|
+
if (discovered.length === 1) {
|
|
36
|
+
const folder = discovered[0];
|
|
37
|
+
console.log(` · ${formatFolderOption(folder)}`);
|
|
38
|
+
|
|
39
|
+
const isDocsFolder = await confirm(
|
|
40
|
+
rl,
|
|
41
|
+
`Is ./${folder.relative}/ your Destinotes docs folder?`,
|
|
42
|
+
true,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (isDocsFolder) {
|
|
46
|
+
selectedRelative = folder.relative;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
selectedRelative = await chooseFromList(
|
|
50
|
+
rl,
|
|
51
|
+
"Which folder holds your Destinotes docs?",
|
|
52
|
+
discovered.map((folder) => ({
|
|
53
|
+
label: formatFolderOption(folder),
|
|
54
|
+
value: folder.relative,
|
|
55
|
+
})),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!selectedRelative) {
|
|
60
|
+
return { docsDirDefault, migrationResult };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isCanonicalDocsDir(selectedRelative)) {
|
|
64
|
+
docsDirDefault = CANONICAL_DOCS_DIR;
|
|
65
|
+
return { docsDirDefault, migrationResult };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const shouldMigrate = await confirm(
|
|
69
|
+
rl,
|
|
70
|
+
`Move everything from ./${selectedRelative}/ to ./${CANONICAL_DOCS_DIR}/?`,
|
|
71
|
+
true,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (shouldMigrate) {
|
|
75
|
+
migrationResult = migrateToCanonical(cwd, selectedRelative);
|
|
76
|
+
docsDirDefault = CANONICAL_DOCS_DIR;
|
|
77
|
+
|
|
78
|
+
if (migrationResult.moved.length > 0) {
|
|
79
|
+
console.log(
|
|
80
|
+
`✓ Moved ${migrationResult.moved.length} item(s) to ./${CANONICAL_DOCS_DIR}/`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (migrationResult.skipped.length > 0) {
|
|
85
|
+
console.log(
|
|
86
|
+
` Skipped (already exists in ./${CANONICAL_DOCS_DIR}/): ${migrationResult.skipped.join(", ")}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
docsDirDefault = selectedRelative;
|
|
91
|
+
console.log(`Using ./${selectedRelative}/ as the docs folder.`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { docsDirDefault, migrationResult };
|
|
95
|
+
}
|
|
96
|
+
|
|
14
97
|
export async function runSetup({
|
|
15
98
|
cwd,
|
|
16
99
|
global = false,
|
|
@@ -23,16 +106,24 @@ export async function runSetup({
|
|
|
23
106
|
const existing = readConfig(cwd);
|
|
24
107
|
|
|
25
108
|
if (yes) {
|
|
109
|
+
const discovered = discoverDestinotesFolders(cwd);
|
|
110
|
+
const docsDir =
|
|
111
|
+
existing?.docsDir ??
|
|
112
|
+
discovered.find((folder) => isCanonicalDocsDir(folder.relative))?.relative ??
|
|
113
|
+
discovered[0]?.relative ??
|
|
114
|
+
CANONICAL_DOCS_DIR;
|
|
115
|
+
|
|
26
116
|
return {
|
|
27
117
|
version: CONFIG_VERSION,
|
|
28
118
|
projectName: existing?.projectName ?? detectProjectName(cwd),
|
|
29
119
|
description: existing?.description ?? "",
|
|
30
120
|
repositoryUrl: existing?.repositoryUrl ?? git.repositoryUrl ?? "",
|
|
31
121
|
defaultBranch: existing?.defaultBranch ?? git.defaultBranch,
|
|
32
|
-
docsDir
|
|
122
|
+
docsDir,
|
|
33
123
|
installedAt: new Date().toISOString(),
|
|
34
124
|
global,
|
|
35
125
|
agents: agents ?? existing?.agents ?? detectedAgents,
|
|
126
|
+
migration: null,
|
|
36
127
|
};
|
|
37
128
|
}
|
|
38
129
|
|
|
@@ -59,10 +150,17 @@ export async function runSetup({
|
|
|
59
150
|
...existing,
|
|
60
151
|
global: existing.global ?? global,
|
|
61
152
|
agents: selectedAgents,
|
|
153
|
+
migration: null,
|
|
62
154
|
};
|
|
63
155
|
}
|
|
64
156
|
}
|
|
65
157
|
|
|
158
|
+
const { docsDirDefault, migrationResult } = await resolveDocsFolder(
|
|
159
|
+
rl,
|
|
160
|
+
cwd,
|
|
161
|
+
existing,
|
|
162
|
+
);
|
|
163
|
+
|
|
66
164
|
const projectName = await ask(
|
|
67
165
|
rl,
|
|
68
166
|
"Project name",
|
|
@@ -84,7 +182,7 @@ export async function runSetup({
|
|
|
84
182
|
const docsDir = await ask(
|
|
85
183
|
rl,
|
|
86
184
|
"Docs output folder (relative to project root)",
|
|
87
|
-
|
|
185
|
+
docsDirDefault,
|
|
88
186
|
);
|
|
89
187
|
|
|
90
188
|
const description = await ask(
|
|
@@ -114,6 +212,7 @@ export async function runSetup({
|
|
|
114
212
|
installedAt: new Date().toISOString(),
|
|
115
213
|
global,
|
|
116
214
|
agents: selectedAgents,
|
|
215
|
+
migration: migrationResult,
|
|
117
216
|
};
|
|
118
217
|
|
|
119
218
|
console.log("\nSetup summary:");
|
|
@@ -121,6 +220,11 @@ export async function runSetup({
|
|
|
121
220
|
console.log(` Repository: ${config.repositoryUrl || "(none)"}`);
|
|
122
221
|
console.log(` Branch: ${config.defaultBranch}`);
|
|
123
222
|
console.log(` Docs folder: ./${config.docsDir}/`);
|
|
223
|
+
if (migrationResult?.migrated) {
|
|
224
|
+
console.log(
|
|
225
|
+
` Migration: ./${migrationResult.source}/ → ./${migrationResult.dest}/`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
124
228
|
console.log(
|
|
125
229
|
` Agents: ${selectedAgents.map((id) => agentLabels[id]).join(", ")}`,
|
|
126
230
|
);
|
|
@@ -143,7 +247,9 @@ export function finalizeProjectFiles(cwd, config, existingConfig) {
|
|
|
143
247
|
return null;
|
|
144
248
|
}
|
|
145
249
|
|
|
146
|
-
|
|
147
|
-
|
|
250
|
+
const { migration: _migration, ...persistedConfig } = config;
|
|
251
|
+
|
|
252
|
+
return scaffoldDocs(cwd, persistedConfig, {
|
|
253
|
+
overwriteReadme: Boolean(existingConfig) || Boolean(config.migration?.migrated),
|
|
148
254
|
});
|
|
149
255
|
}
|