docugate 0.2.9 → 0.3.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/README.md +200 -57
- package/dist/api.d.ts +44 -0
- package/dist/api.js +11 -5
- package/dist/args.d.ts +29 -0
- package/dist/args.js +121 -0
- package/dist/auth.d.ts +21 -0
- package/dist/banner.d.ts +13 -0
- package/dist/banner.js +62 -0
- package/dist/check.d.ts +22 -0
- package/dist/check.js +4 -3
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +201 -165
- package/dist/commands.d.ts +35 -0
- package/dist/commands.js +293 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +43 -3
- package/dist/developers.d.ts +35 -0
- package/dist/developers.js +439 -0
- package/dist/init.d.ts +130 -0
- package/dist/init.js +93 -32
- package/dist/keys/core.d.ts +108 -0
- package/dist/keys/core.js +142 -0
- package/dist/keys/express.d.ts +21 -0
- package/dist/keys/express.js +27 -0
- package/dist/keys/fastify.d.ts +21 -0
- package/dist/keys/fastify.js +21 -0
- package/dist/keys/format.d.ts +11 -0
- package/dist/keys/format.js +46 -0
- package/dist/keys/hono.d.ts +19 -0
- package/dist/keys/hono.js +21 -0
- package/dist/keys/index.d.ts +13 -0
- package/dist/keys/index.js +12 -0
- package/dist/logo-pixels.d.ts +4 -0
- package/dist/logo-pixels.js +35 -0
- package/dist/logo.d.ts +19 -0
- package/dist/logo.js +68 -0
- package/dist/menu.d.ts +16 -0
- package/dist/menu.js +157 -0
- package/dist/openapi.d.ts +38 -0
- package/dist/openapi.js +2 -1
- package/dist/rules.d.ts +69 -0
- package/dist/rules.js +2 -1
- package/dist/select.d.ts +75 -0
- package/dist/select.js +215 -0
- package/dist/session.d.ts +46 -0
- package/dist/session.js +54 -0
- package/dist/spinner.d.ts +12 -0
- package/dist/spinner.js +12 -12
- package/dist/status.d.ts +46 -0
- package/dist/status.js +166 -0
- package/dist/sync/ai.d.ts +44 -0
- package/dist/sync/ai.js +147 -0
- package/dist/sync/apply.d.ts +25 -0
- package/dist/sync/apply.js +104 -0
- package/dist/sync/changes.d.ts +58 -0
- package/dist/sync/changes.js +167 -0
- package/dist/sync/command.d.ts +12 -0
- package/dist/sync/command.js +236 -0
- package/dist/sync/guard.d.ts +15 -0
- package/dist/sync/guard.js +21 -0
- package/dist/sync/hook.d.ts +10 -0
- package/dist/sync/hook.js +62 -0
- package/dist/sync/impact.d.ts +23 -0
- package/dist/sync/impact.js +93 -0
- package/dist/sync/pr.d.ts +37 -0
- package/dist/sync/pr.js +106 -0
- package/dist/sync/run.d.ts +50 -0
- package/dist/sync/run.js +124 -0
- package/dist/sync/state.d.ts +8 -0
- package/dist/sync/state.js +22 -0
- package/dist/sync/workflow.d.ts +20 -0
- package/dist/sync/workflow.js +84 -0
- package/dist/text.d.ts +11 -0
- package/dist/text.js +16 -0
- package/dist/tour-init.d.ts +61 -0
- package/dist/tour-install.d.ts +38 -0
- package/dist/tour-server.d.ts +21 -0
- package/dist/tour.d.ts +60 -0
- package/dist/ui.d.ts +29 -0
- package/dist/ui.js +63 -0
- package/package.json +67 -47
package/dist/sync/run.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { checkTours } from '../check.js';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
import { planEdits, providerAiFn, resolveProvider, KEY_HELP } from './ai.js';
|
|
4
|
+
import { applyEdits, validateEdits } from './apply.js';
|
|
5
|
+
import { collectChanges, defaultGit, SyncError } from './changes.js';
|
|
6
|
+
import { shouldSkip } from './guard.js';
|
|
7
|
+
import { loadPages, scoreImpact } from './impact.js';
|
|
8
|
+
import { buildPrBody, defaultPr } from './pr.js';
|
|
9
|
+
import { readSyncState, STATE_FILE, writeSyncState } from './state.js';
|
|
10
|
+
const DEFAULT_MAX_FILES = 10;
|
|
11
|
+
export async function runSync(root, options = {}, deps = {}) {
|
|
12
|
+
const env = deps.env ?? process.env;
|
|
13
|
+
const git = deps.git ?? defaultGit;
|
|
14
|
+
const log = deps.log ?? (() => { });
|
|
15
|
+
const result = { dryRun: Boolean(options.dryRun), staleTour: [], written: [], rejected: [] };
|
|
16
|
+
const loaded = loadConfig(root);
|
|
17
|
+
if (loaded.errors.length) {
|
|
18
|
+
throw new SyncError(`docugate.json has a problem: ${loaded.errors.map((e) => e.message).join(' ')}`);
|
|
19
|
+
}
|
|
20
|
+
const { docsDir } = loaded;
|
|
21
|
+
const sync = loaded.config.sync ?? {};
|
|
22
|
+
const tryGit = (args) => {
|
|
23
|
+
try {
|
|
24
|
+
return git(args, root).trim();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const branch = options.branch ?? env.GITHUB_REF_NAME ?? tryGit(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
31
|
+
const early = shouldSkip({ headMessage: tryGit(['log', '-1', '--format=%B']), branch, docsDir });
|
|
32
|
+
if (early)
|
|
33
|
+
return { ...result, skipped: `Skipped: ${early}` };
|
|
34
|
+
const state = readSyncState(root);
|
|
35
|
+
const changes = collectChanges(root, {
|
|
36
|
+
since: options.since,
|
|
37
|
+
lastSha: state.lastSha,
|
|
38
|
+
freshness: loaded.config.freshness,
|
|
39
|
+
docsDir,
|
|
40
|
+
include: sync.include,
|
|
41
|
+
exclude: sync.exclude,
|
|
42
|
+
git,
|
|
43
|
+
now: deps.now,
|
|
44
|
+
});
|
|
45
|
+
result.changes = changes;
|
|
46
|
+
const pages = loadPages(root, docsDir);
|
|
47
|
+
result.impact = scoreImpact(pages, changes.files, sync.map);
|
|
48
|
+
result.staleTour = checkTours(root);
|
|
49
|
+
if (changes.files.length === 0) {
|
|
50
|
+
const onlyDocs = changes.skipped.length > 0 && changes.skipped.every((s) => /documentation|DocuGate's own/.test(s.reason));
|
|
51
|
+
return {
|
|
52
|
+
...result,
|
|
53
|
+
skipped: onlyDocs
|
|
54
|
+
? 'Skipped: the changes only touch documentation.'
|
|
55
|
+
: `Nothing to document ${changes.range}: no code changed.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (options.dryRun)
|
|
59
|
+
return result;
|
|
60
|
+
// From here the changes leave the machine, so the provider has to be known.
|
|
61
|
+
const provider = resolveProvider(env, sync, options.model);
|
|
62
|
+
const aiFn = deps.aiFn ?? (provider ? providerAiFn(provider) : undefined);
|
|
63
|
+
if (!aiFn)
|
|
64
|
+
throw new SyncError(`No AI key found. ${KEY_HELP}`);
|
|
65
|
+
result.provider = provider ? `${provider.name} (${provider.model})` : 'custom';
|
|
66
|
+
if (deps.confirm && !options.yes) {
|
|
67
|
+
const ok = await deps.confirm(`This sends the code changes (${changes.files.length} file${changes.files.length === 1 ? '' : 's'}) and the affected docs to ${result.provider}. Continue?`);
|
|
68
|
+
if (!ok)
|
|
69
|
+
return { ...result, skipped: 'Stopped before anything was sent.' };
|
|
70
|
+
}
|
|
71
|
+
const maxFiles = options.maxFiles ?? sync.maxFiles ?? DEFAULT_MAX_FILES;
|
|
72
|
+
log('Asking the model what the docs need…');
|
|
73
|
+
const plan = await planEdits(aiFn, { changes, impact: result.impact, pages, docsDir, maxFiles });
|
|
74
|
+
result.summary = plan.summary;
|
|
75
|
+
const checked = validateEdits(root, docsDir, plan.edits, { maxFiles, pageCount: pages.length });
|
|
76
|
+
result.rejected.push(...checked.rejected);
|
|
77
|
+
const applied = applyEdits(root, checked.accepted);
|
|
78
|
+
result.rejected.push(...applied.reverted);
|
|
79
|
+
result.written = applied.written;
|
|
80
|
+
if (sync.digest && applied.written.length) {
|
|
81
|
+
const date = (deps.now ?? new Date()).toISOString().slice(0, 10);
|
|
82
|
+
const digest = digestPage(changes, applied.written, plan.summary, date);
|
|
83
|
+
const path = `${docsDir === '.' ? '' : `${docsDir}/`}changes/${date}.md`;
|
|
84
|
+
const done = applyEdits(root, [{ path, action: 'create', content: digest, reason: 'what changed this run' }]);
|
|
85
|
+
result.written.push(...done.written);
|
|
86
|
+
}
|
|
87
|
+
if (result.written.length === 0)
|
|
88
|
+
return result;
|
|
89
|
+
// The sync moves forward only when there is something to review.
|
|
90
|
+
writeSyncState(root, { lastSha: changes.to, lastRunAt: (deps.now ?? new Date()).toISOString() });
|
|
91
|
+
if (options.pr) {
|
|
92
|
+
const prFn = deps.prFn ?? defaultPr({ git, env });
|
|
93
|
+
const siteUrl = env.DOCUGATE_URL ?? 'https://www.trydocugate.site';
|
|
94
|
+
result.pr = await prFn({
|
|
95
|
+
root,
|
|
96
|
+
title: `docs: sync with code changes (${changes.commits.length} commit${changes.commits.length === 1 ? '' : 's'})`,
|
|
97
|
+
body: buildPrBody({
|
|
98
|
+
summary: plan.summary,
|
|
99
|
+
written: result.written,
|
|
100
|
+
rejected: result.rejected,
|
|
101
|
+
changes,
|
|
102
|
+
docsDir,
|
|
103
|
+
space: loaded.config.space,
|
|
104
|
+
siteUrl,
|
|
105
|
+
staleTour: result.staleTour.map((t) => `${t.file ? `${t.file}: ` : ''}${t.message}`),
|
|
106
|
+
}),
|
|
107
|
+
files: [...result.written.map((e) => e.path), STATE_FILE],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
/** A short, plain page of what changed, made without the model. */
|
|
113
|
+
function digestPage(changes, written, summary, date) {
|
|
114
|
+
const lines = [`# Changes on ${date}`, ''];
|
|
115
|
+
if (summary)
|
|
116
|
+
lines.push(summary, '');
|
|
117
|
+
lines.push('## Commits', '');
|
|
118
|
+
for (const c of changes.commits.slice(0, 50))
|
|
119
|
+
lines.push(`- ${c.subject} (${c.author})`);
|
|
120
|
+
lines.push('', '## Pages updated', '');
|
|
121
|
+
for (const e of written)
|
|
122
|
+
lines.push(`- \`${e.path}\`${e.reason ? `: ${e.reason}` : ''}`);
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Where the last sync ended. It is committed in the sync's own pull request. */
|
|
2
|
+
export declare const STATE_FILE = ".docugate/sync.json";
|
|
3
|
+
export type SyncState = {
|
|
4
|
+
lastSha?: string;
|
|
5
|
+
lastRunAt?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function readSyncState(root: string): SyncState;
|
|
8
|
+
export declare function writeSyncState(root: string, state: SyncState): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
/** Where the last sync ended. It is committed in the sync's own pull request. */
|
|
4
|
+
export const STATE_FILE = '.docugate/sync.json';
|
|
5
|
+
export function readSyncState(root) {
|
|
6
|
+
const path = join(root, STATE_FILE);
|
|
7
|
+
if (!existsSync(path))
|
|
8
|
+
return {};
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
11
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// A damaged file is the same as none: the next run looks back a default window.
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function writeSyncState(root, state) {
|
|
19
|
+
const path = join(root, STATE_FILE);
|
|
20
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
21
|
+
writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`);
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DocugateConfig } from '../rules.js';
|
|
2
|
+
export declare const WORKFLOW_FILE = ".github/workflows/docugate-sync.yml";
|
|
3
|
+
export type WorkflowOptions = {
|
|
4
|
+
freshness: NonNullable<DocugateConfig['freshness']>;
|
|
5
|
+
docsDir: string;
|
|
6
|
+
/** The branch pushes are watched on. */
|
|
7
|
+
branch: string;
|
|
8
|
+
/** The version to run, so a new release never changes what CI does by surprise. */
|
|
9
|
+
version: string;
|
|
10
|
+
provider?: 'anthropic' | 'openai';
|
|
11
|
+
};
|
|
12
|
+
/** The workflow file, or null when `freshness` is "off". */
|
|
13
|
+
export declare function buildWorkflow(o: WorkflowOptions): string | null;
|
|
14
|
+
export type WorkflowResult = {
|
|
15
|
+
path: string;
|
|
16
|
+
status: 'created' | 'updated' | 'kept' | 'off';
|
|
17
|
+
};
|
|
18
|
+
/** Write the workflow. An existing one that differs is left alone unless `force`. */
|
|
19
|
+
export declare function writeWorkflow(root: string, o: WorkflowOptions, force?: boolean): WorkflowResult;
|
|
20
|
+
export declare function workflowInstalled(root: string): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { SYNC_BRANCH_PREFIX, SYNC_TAG } from './guard.js';
|
|
4
|
+
// The GitHub Action that runs `docugate sync` for you. How often is the
|
|
5
|
+
// `freshness` setting in docugate.json: on every push, daily or weekly.
|
|
6
|
+
export const WORKFLOW_FILE = '.github/workflows/docugate-sync.yml';
|
|
7
|
+
const CRON = {
|
|
8
|
+
// Off the hour, where GitHub's schedules are busiest.
|
|
9
|
+
daily: '17 6 * * *',
|
|
10
|
+
weekly: '17 6 * * 1',
|
|
11
|
+
};
|
|
12
|
+
/** The workflow file, or null when `freshness` is "off". */
|
|
13
|
+
export function buildWorkflow(o) {
|
|
14
|
+
if (o.freshness === 'off')
|
|
15
|
+
return null;
|
|
16
|
+
const secret = o.provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
|
|
17
|
+
const docs = o.docsDir === '.' ? null : o.docsDir.replace(/^\/|\/$/g, '');
|
|
18
|
+
const on = o.freshness === 'push'
|
|
19
|
+
? [
|
|
20
|
+
' push:',
|
|
21
|
+
` branches: [${o.branch}]`,
|
|
22
|
+
' paths-ignore:',
|
|
23
|
+
...(docs ? [` - '${docs}/**'`] : []),
|
|
24
|
+
` - '.docugate/**'`,
|
|
25
|
+
]
|
|
26
|
+
: [' schedule:', ` - cron: '${CRON[o.freshness]}'`];
|
|
27
|
+
return `# Written by \`docugate sync init\`. It keeps your docs in step with your code.
|
|
28
|
+
# ${o.freshness === 'push' ? 'Runs on every push' : o.freshness === 'daily' ? 'Runs every day' : 'Runs every Monday'}, reads what changed, and opens a pull request with the docs
|
|
29
|
+
# edits. Nothing is merged for you. Change how often in docugate.json ("freshness"),
|
|
30
|
+
# then run \`docugate sync init --force\`.
|
|
31
|
+
name: DocuGate sync
|
|
32
|
+
|
|
33
|
+
on:
|
|
34
|
+
${on.join('\n')}
|
|
35
|
+
workflow_dispatch:
|
|
36
|
+
|
|
37
|
+
permissions:
|
|
38
|
+
contents: write
|
|
39
|
+
pull-requests: write
|
|
40
|
+
|
|
41
|
+
# One sync at a time, so two runs never fight over the same branch.
|
|
42
|
+
concurrency:
|
|
43
|
+
group: docugate-sync
|
|
44
|
+
cancel-in-progress: false
|
|
45
|
+
|
|
46
|
+
jobs:
|
|
47
|
+
sync:
|
|
48
|
+
# A sync's own commits and branch never start another sync.
|
|
49
|
+
if: \${{ !contains(github.event.head_commit.message, '${SYNC_TAG}') && !startsWith(github.ref_name, '${SYNC_BRANCH_PREFIX}') }}
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
with:
|
|
54
|
+
fetch-depth: 0
|
|
55
|
+
- uses: actions/setup-node@v4
|
|
56
|
+
with:
|
|
57
|
+
node-version: 22
|
|
58
|
+
- run: npx --yes docugate@${o.version} sync --pr --yes
|
|
59
|
+
env:
|
|
60
|
+
${secret}: \${{ secrets.${secret} }}
|
|
61
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
/** Write the workflow. An existing one that differs is left alone unless `force`. */
|
|
65
|
+
export function writeWorkflow(root, o, force = false) {
|
|
66
|
+
const text = buildWorkflow(o);
|
|
67
|
+
if (text === null)
|
|
68
|
+
return { path: WORKFLOW_FILE, status: 'off' };
|
|
69
|
+
const path = join(root, WORKFLOW_FILE);
|
|
70
|
+
if (existsSync(path)) {
|
|
71
|
+
if (readFileSync(path, 'utf8') === text)
|
|
72
|
+
return { path: WORKFLOW_FILE, status: 'kept' };
|
|
73
|
+
if (!force)
|
|
74
|
+
return { path: WORKFLOW_FILE, status: 'kept' };
|
|
75
|
+
writeFileSync(path, text);
|
|
76
|
+
return { path: WORKFLOW_FILE, status: 'updated' };
|
|
77
|
+
}
|
|
78
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
79
|
+
writeFileSync(path, text);
|
|
80
|
+
return { path: WORKFLOW_FILE, status: 'created' };
|
|
81
|
+
}
|
|
82
|
+
export function workflowInstalled(root) {
|
|
83
|
+
return existsSync(join(root, WORKFLOW_FILE));
|
|
84
|
+
}
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a file somebody wrote by hand, as DocuGate's server will read it.
|
|
3
|
+
*
|
|
4
|
+
* Windows PowerShell and Notepad save UTF-8 files with a byte-order mark. The server
|
|
5
|
+
* decodes what GitHub sends with `Response.text()`, which drops the mark, so such a file
|
|
6
|
+
* works there. `readFileSync` keeps it as a stray U+FEFF at the front, which breaks
|
|
7
|
+
* `JSON.parse` and hides a leading `# Title`, so the CLI would report a problem the
|
|
8
|
+
* published space does not have. Stripping it here keeps `docugate check` honest.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readText(path: string): string;
|
|
11
|
+
export declare function stripBom(text: string): string;
|
package/dist/text.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Reads a file somebody wrote by hand, as DocuGate's server will read it.
|
|
4
|
+
*
|
|
5
|
+
* Windows PowerShell and Notepad save UTF-8 files with a byte-order mark. The server
|
|
6
|
+
* decodes what GitHub sends with `Response.text()`, which drops the mark, so such a file
|
|
7
|
+
* works there. `readFileSync` keeps it as a stray U+FEFF at the front, which breaks
|
|
8
|
+
* `JSON.parse` and hides a leading `# Title`, so the CLI would report a problem the
|
|
9
|
+
* published space does not have. Stripping it here keeps `docugate check` honest.
|
|
10
|
+
*/
|
|
11
|
+
export function readText(path) {
|
|
12
|
+
return stripBom(readFileSync(path, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
export function stripBom(text) {
|
|
15
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare const TOUR_DIR = ".docugate/tour";
|
|
2
|
+
export declare const MODE_SLUG = "tour-writer";
|
|
3
|
+
export declare const TOUR_RULES = "# DocuGate tour format\n\nA tour explains the screens of an app to a developer who is new to its code.\nIt lives in `.docugate/tour/`, one markdown file per screen.\n\n## File\n\n- Name the file after the screen's route: `/invoices/:id` becomes\n `invoices-id.md`, `/` becomes `home.md`.\n- Front matter holds `route` (the URL pattern, with `:param` segments) and\n `title` (the screen's name).\n- Then one `##` section per stop, in the order a person reads the screen.\n\n## Stop\n\n```md\n## Total\ntarget: [data-tour=\"invoice-total\"]\ndata: GET /api/invoices/:id \u2192 total\ncode: frontend/src/screens/InvoiceDetail.tsx\nsource: backend/src/billing.ts\ndocs: https://example.com/docs/billing\n\nThe amount due: the subtotal plus tax, computed by `totals()` in the backend.\n```\n\n- `target`: a CSS selector for the element. Prefer `[data-tour=\"...\"]`\n when the element has one. Otherwise use the most stable selector you can\n see, and add a line `suggest: add data-tour=\"name\" in <file>`.\n- `data`: the request the value comes from and the field in the response,\n as `METHOD /path \u2192 field`. Leave it out for elements that show no data.\n- `code`: the file that renders the element, relative to the repository root.\n- `source`: optional. The file where the value is computed or stored, when\n that is somewhere else, such as a backend service.\n- `docs`: optional. A link that explains the concept.\n- Then one or two sentences of plain prose: what the element is and why its\n value is what it is.\n\n## Rules\n\n- Only claim what the code shows. Every path must exist and every endpoint\n must appear in the code. If you are not sure where a value comes from,\n leave `data` and `source` out rather than guess.\n- Pick the elements a new developer would ask about: numbers, statuses,\n actions and anything whose meaning is not obvious. Not every element.\n- Never edit, rename or delete a tour file that already exists. Only add\n files for screens that have none.\n- Change nothing outside `.docugate/tour/`.\n";
|
|
4
|
+
export declare const MODE: {
|
|
5
|
+
slug: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
roleDefinition: string;
|
|
9
|
+
whenToUse: string;
|
|
10
|
+
customInstructions: string;
|
|
11
|
+
groups: (string | (string | {
|
|
12
|
+
fileRegex: string;
|
|
13
|
+
description: string;
|
|
14
|
+
})[])[];
|
|
15
|
+
};
|
|
16
|
+
export interface BobRun {
|
|
17
|
+
status: number | null;
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
}
|
|
21
|
+
export type RunBob = (args: string[], cwd: string) => BobRun;
|
|
22
|
+
export interface TourInitOptions {
|
|
23
|
+
maxCost?: string;
|
|
24
|
+
teamId?: string;
|
|
25
|
+
runBob?: RunBob;
|
|
26
|
+
}
|
|
27
|
+
export interface TourInitResult {
|
|
28
|
+
setup: string[];
|
|
29
|
+
added: string[];
|
|
30
|
+
restored: string[];
|
|
31
|
+
mode: string;
|
|
32
|
+
stats?: {
|
|
33
|
+
durationMs?: number;
|
|
34
|
+
cost?: number;
|
|
35
|
+
taskId?: string;
|
|
36
|
+
};
|
|
37
|
+
lastMessage?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare class BobMissingError extends Error {
|
|
40
|
+
}
|
|
41
|
+
/** Bob Shell's headless mode signs in with an API key, not the IDE's IBMid session. */
|
|
42
|
+
export declare class BobKeyMissingError extends Error {
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The Bob API key comes from the environment only: BOB_API_KEY. DocuGate never
|
|
46
|
+
* writes it anywhere. A key pasted during setup is put in this process's
|
|
47
|
+
* environment for that one run, and is gone when the command ends.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readBobKey(): string | undefined;
|
|
50
|
+
export declare function useBobKeyForThisRun(key: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* On Windows, `bob` is an npm .cmd shim, and a .cmd only runs through cmd.exe,
|
|
53
|
+
* which splits a path at its spaces ("C:\Users\GUSENGA Thierry") and cannot
|
|
54
|
+
* carry the prompt's line breaks. The shim only starts Node on Bob Shell's
|
|
55
|
+
* script, so run that script with Node directly: no shell, nothing mangled.
|
|
56
|
+
*/
|
|
57
|
+
export declare function bobScript(pathEnv?: string): string | undefined;
|
|
58
|
+
/** Adds the Tour Writer mode and its rules to the project, keeping whatever is there. */
|
|
59
|
+
export declare function ensureTourWriter(root: string): string[];
|
|
60
|
+
export declare function buildPrompt(existing: string[], inlineRules: boolean): string;
|
|
61
|
+
export declare function tourInit(root: string, options?: TourInitOptions): TourInitResult;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const DEFAULT_PORT = 4178;
|
|
2
|
+
/** Candidate paths checked in order. The first one that exists wins. */
|
|
3
|
+
export declare const CANDIDATES: readonly ["app/layout.tsx", "app/layout.jsx", "src/app/layout.tsx", "src/app/layout.jsx", "pages/_document.tsx", "pages/_document.jsx", "index.html", "frontend/index.html", "client/index.html", "web/index.html", "app/index.html", "public/index.html"];
|
|
4
|
+
export type TourInstallOptions = {
|
|
5
|
+
port?: number;
|
|
6
|
+
remove?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export type TourInstallResult = {
|
|
9
|
+
/** The file that was (or would be) modified; null when nothing was found. */
|
|
10
|
+
target: string | null;
|
|
11
|
+
action: 'inserted' | 'removed' | 'already' | 'none';
|
|
12
|
+
/** The npm script that now starts the tour server with the app, e.g. predev. */
|
|
13
|
+
hook?: string;
|
|
14
|
+
hookAction?: 'inserted' | 'removed' | 'already' | 'none';
|
|
15
|
+
};
|
|
16
|
+
/** What the app's own start script runs first, so the tour server comes up with it. */
|
|
17
|
+
export declare const SERVE_HOOK = "docugate tour serve --background";
|
|
18
|
+
/**
|
|
19
|
+
* Adds `docugate tour serve --background` as the `predev` script (or
|
|
20
|
+
* `prestart`, for apps started with npm start), so running the project the
|
|
21
|
+
* usual way also starts the tour server. npm runs pre-scripts by itself, on
|
|
22
|
+
* every platform. An existing pre-script keeps running first. Only this
|
|
23
|
+
* repository's package.json, and only its scripts, are touched.
|
|
24
|
+
*/
|
|
25
|
+
export declare function installStartHook(root: string, options?: TourInstallOptions): {
|
|
26
|
+
hook?: string;
|
|
27
|
+
action: TourInstallResult['action'];
|
|
28
|
+
};
|
|
29
|
+
/** Returns true if any candidate file already contains the pill snippet. */
|
|
30
|
+
export declare function hasPillSnippet(root: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Installs (or removes) the dev-only pill loader snippet in the app's entry
|
|
33
|
+
* point. Safe to run multiple times — running it again when the snippet is
|
|
34
|
+
* already present is a no-op.
|
|
35
|
+
*/
|
|
36
|
+
export declare function tourInstall(root: string, options?: TourInstallOptions): TourInstallResult;
|
|
37
|
+
/** The one-line snippet to paste manually when no entry point was found. */
|
|
38
|
+
export declare function pillSnippetLine(port: number): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Where this project's docs are published, from the space docugate init saved. */
|
|
2
|
+
export declare function publishedDocsUrl(root: string): string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* The repository file that renders an element with this data-tour value, so
|
|
5
|
+
* the pill fills in "the file that shows it" instead of asking. A file with
|
|
6
|
+
* data-tour="value" wins over one that only passes the value along as a prop.
|
|
7
|
+
*/
|
|
8
|
+
export declare function findTourFile(root: string, value: string): string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* The editor `docugate tour serve` was started from, as the command that opens
|
|
11
|
+
* a file at a line. DOCUGATE_EDITOR wins; otherwise the integrated terminal's
|
|
12
|
+
* environment says which VS Code family editor it is.
|
|
13
|
+
*/
|
|
14
|
+
export declare function editorCommand(env?: NodeJS.ProcessEnv): string;
|
|
15
|
+
/** The 1-based line of the first occurrence of `find` in `text`, or 1. */
|
|
16
|
+
export declare function lineOf(text: string, find?: string): number;
|
|
17
|
+
export interface TourServer {
|
|
18
|
+
port: number;
|
|
19
|
+
close(): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function startTourServer(root: string, port: number): Promise<TourServer>;
|
package/dist/tour.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type TourStop = {
|
|
2
|
+
/** Heading text for this stop (the ## heading). */
|
|
3
|
+
heading: string;
|
|
4
|
+
/** CSS selector for the element, e.g. `[data-tour="invoice-total"]`. */
|
|
5
|
+
target: string;
|
|
6
|
+
/** Request and field, e.g. `GET /api/invoices/:id → total`. Optional. */
|
|
7
|
+
data?: string;
|
|
8
|
+
/** Repo-relative path to the file that renders the element. */
|
|
9
|
+
code: string;
|
|
10
|
+
/** Repo-relative path to the file where the value is computed. Optional. */
|
|
11
|
+
source?: string;
|
|
12
|
+
/** URL linking to relevant docs. Optional. */
|
|
13
|
+
docs?: string;
|
|
14
|
+
/** Prose paragraph(s) after the fields. */
|
|
15
|
+
prose: string;
|
|
16
|
+
};
|
|
17
|
+
export type Tour = {
|
|
18
|
+
/** URL pattern for the screen, e.g. `/invoices/:id`. */
|
|
19
|
+
route: string;
|
|
20
|
+
/** Human-readable title for the screen. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** Stops, in order. */
|
|
23
|
+
stops: TourStop[];
|
|
24
|
+
/** Source file path, set by loadTours. */
|
|
25
|
+
file?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parse a tour markdown file into a Tour object.
|
|
29
|
+
* `text` is the file contents, `file` is the path (used for error messages).
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseTourFile(text: string, file: string): Tour;
|
|
32
|
+
/**
|
|
33
|
+
* Serialize a Tour back to the markdown format parseTourFile reads.
|
|
34
|
+
*/
|
|
35
|
+
export declare function serializeTourFile(tour: Tour): string;
|
|
36
|
+
/**
|
|
37
|
+
* Read every .md file under `<root>/.docugate/tour/` and return an array of
|
|
38
|
+
* Tour objects. Returns [] when the directory does not exist.
|
|
39
|
+
*/
|
|
40
|
+
export declare function loadTours(root: string): Tour[];
|
|
41
|
+
/**
|
|
42
|
+
* Match a tour route pattern (with `:param` segments) against a pathname.
|
|
43
|
+
* Returns true when they match.
|
|
44
|
+
*
|
|
45
|
+
* Examples:
|
|
46
|
+
* matchRoute('/invoices/:id', '/invoices/INV-001') → true
|
|
47
|
+
* matchRoute('/invoices/:id', '/invoices') → false
|
|
48
|
+
*/
|
|
49
|
+
export declare function matchRoute(route: string, pathname: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Derive the tour filename from a route.
|
|
52
|
+
* Strips the leading `/`, replaces `/` and `:` with `-`, lower-cases the result,
|
|
53
|
+
* and appends `.md`. The root route `/` becomes `home.md`.
|
|
54
|
+
*
|
|
55
|
+
* Examples:
|
|
56
|
+
* routeToFilename('/') → 'home.md'
|
|
57
|
+
* routeToFilename('/invoices/:id') → 'invoices-id.md'
|
|
58
|
+
* routeToFilename('/orgs/:org/repos/:repo') → 'orgs-org-repos-repo.md'
|
|
59
|
+
*/
|
|
60
|
+
export declare function routeToFilename(route: string): string;
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Color only in a real terminal, and never when NO_COLOR is set, so logs and CI stay plain. */
|
|
2
|
+
/**
|
|
3
|
+
* Git Bash's mintty window draws color, but Node sees its console as a pipe,
|
|
4
|
+
* so it is recognised by the name it puts in TERM_PROGRAM.
|
|
5
|
+
*/
|
|
6
|
+
export declare const isMintty: () => boolean;
|
|
7
|
+
export declare const colorOn: () => boolean;
|
|
8
|
+
export declare const bold: (text: string) => string;
|
|
9
|
+
export declare const dim: (text: string) => string;
|
|
10
|
+
export declare const red: (text: string) => string;
|
|
11
|
+
export declare const green: (text: string) => string;
|
|
12
|
+
export declare const yellow: (text: string) => string;
|
|
13
|
+
export declare const gold: (text: string) => string;
|
|
14
|
+
export declare const cream: (text: string) => string;
|
|
15
|
+
export declare const plural: (n: number, word: string) => string;
|
|
16
|
+
/** `a`, `a and b`, `a, b and c`. */
|
|
17
|
+
export declare function list(items: string[]): string;
|
|
18
|
+
/**
|
|
19
|
+
* How to write a docugate command for the person reading it: installed in the
|
|
20
|
+
* project (npm i docugate) it runs through npx; installed on the computer it
|
|
21
|
+
* is plain `docugate`. Judged from where this copy of the CLI lives.
|
|
22
|
+
*/
|
|
23
|
+
export declare function asCommand(line: string, script?: string): string;
|
|
24
|
+
/** A command to run next, written the way this person would type it. */
|
|
25
|
+
export declare const hint: (command: string) => string;
|
|
26
|
+
/** Edit distance where swapping two neighbours counts once, so "chekc" is one slip from "check". */
|
|
27
|
+
export declare function distance(a: string, b: string): number;
|
|
28
|
+
/** The closest of `candidates` to `input`, if any is close enough to be a typo. */
|
|
29
|
+
export declare function suggest(input: string, candidates: string[]): string | undefined;
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// What every command prints with: color that turns itself off, and the small
|
|
2
|
+
// helpers that keep the wording the same everywhere.
|
|
3
|
+
/** Color only in a real terminal, and never when NO_COLOR is set, so logs and CI stay plain. */
|
|
4
|
+
/**
|
|
5
|
+
* Git Bash's mintty window draws color, but Node sees its console as a pipe,
|
|
6
|
+
* so it is recognised by the name it puts in TERM_PROGRAM.
|
|
7
|
+
*/
|
|
8
|
+
export const isMintty = () => process.platform === 'win32' && process.env.TERM_PROGRAM === 'mintty';
|
|
9
|
+
export const colorOn = () => (Boolean(process.stdout.isTTY) || isMintty()) && !process.env.NO_COLOR;
|
|
10
|
+
const paint = (code) => (text) => (colorOn() ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
11
|
+
export const bold = paint('1');
|
|
12
|
+
export const dim = paint('2');
|
|
13
|
+
export const red = paint('31');
|
|
14
|
+
export const green = paint('32');
|
|
15
|
+
export const yellow = paint('33');
|
|
16
|
+
export const gold = paint('38;2;244;196;63');
|
|
17
|
+
export const cream = paint('38;2;245;240;227');
|
|
18
|
+
export const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
19
|
+
/** `a`, `a and b`, `a, b and c`. */
|
|
20
|
+
export function list(items) {
|
|
21
|
+
if (items.length < 2)
|
|
22
|
+
return items.join('');
|
|
23
|
+
return `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* How to write a docugate command for the person reading it: installed in the
|
|
27
|
+
* project (npm i docugate) it runs through npx; installed on the computer it
|
|
28
|
+
* is plain `docugate`. Judged from where this copy of the CLI lives.
|
|
29
|
+
*/
|
|
30
|
+
export function asCommand(line, script = process.argv[1] ?? '') {
|
|
31
|
+
if (!/[\\/]node_modules[\\/]/.test(script) || /[\\/]npm[\\/]node_modules[\\/]|[\\/]lib[\\/]node_modules[\\/]/.test(script))
|
|
32
|
+
return line;
|
|
33
|
+
return line.replace(/(^|[\s(`"'])docugate (init|login|logout|whoami|check|openapi|tour|sync|status|help|completion)\b/g, '$1npx docugate $2');
|
|
34
|
+
}
|
|
35
|
+
/** A command to run next, written the way this person would type it. */
|
|
36
|
+
export const hint = (command) => asCommand(`docugate ${command}`);
|
|
37
|
+
/** Edit distance where swapping two neighbours counts once, so "chekc" is one slip from "check". */
|
|
38
|
+
export function distance(a, b) {
|
|
39
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
40
|
+
for (let j = 0; j <= b.length; j++)
|
|
41
|
+
d[0][j] = j;
|
|
42
|
+
for (let i = 1; i <= a.length; i++) {
|
|
43
|
+
for (let j = 1; j <= b.length; j++) {
|
|
44
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
45
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
46
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1])
|
|
47
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return d[a.length][b.length];
|
|
51
|
+
}
|
|
52
|
+
/** The closest of `candidates` to `input`, if any is close enough to be a typo. */
|
|
53
|
+
export function suggest(input, candidates) {
|
|
54
|
+
const lower = input.toLowerCase();
|
|
55
|
+
let best;
|
|
56
|
+
for (const name of candidates) {
|
|
57
|
+
const d = name.startsWith(lower) && lower.length >= 2 ? 1 : distance(lower, name.toLowerCase());
|
|
58
|
+
if (!best || d < best.d)
|
|
59
|
+
best = { name, d };
|
|
60
|
+
}
|
|
61
|
+
const limit = Math.max(1, Math.min(3, Math.floor(lower.length / 3)));
|
|
62
|
+
return best && best.d <= limit ? best.name : undefined;
|
|
63
|
+
}
|