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 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`. Skills are copied into `.cursor/skills/` and/or `.claude/skills/`.
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
@@ -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 = "destinotes") {
12
- const configPath = getConfigPath(cwd, docsDir);
13
- if (!existsSync(configPath)) {
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
- return JSON.parse(readFileSync(configPath, "utf8"));
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
+ }
@@ -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: existing?.docsDir ?? "destinotes",
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
- existing?.docsDir ?? "destinotes",
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
- return scaffoldDocs(cwd, config, {
147
- overwriteReadme: Boolean(existingConfig),
250
+ const { migration: _migration, ...persistedConfig } = config;
251
+
252
+ return scaffoldDocs(cwd, persistedConfig, {
253
+ overwriteReadme: Boolean(existingConfig) || Boolean(config.migration?.migrated),
148
254
  });
149
255
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "destinotes-skills",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Agent skills that generate repository documentation under ./destinotes/",
5
5
  "license": "MIT",
6
6
  "type": "module",