@dinoxx/dinox-cli 2.0.0 → 2.1.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.
Files changed (48) hide show
  1. package/dist/commands/notes/repo/read.d.ts +8 -0
  2. package/dist/commands/notes/repo/read.js +321 -0
  3. package/dist/commands/notes/repo/resolve.d.ts +8 -0
  4. package/dist/commands/notes/repo/resolve.js +187 -0
  5. package/dist/commands/notes/repo/search.d.ts +42 -0
  6. package/dist/commands/notes/repo/search.js +513 -0
  7. package/dist/commands/notes/repo/shared.d.ts +190 -0
  8. package/dist/commands/notes/repo/shared.js +102 -0
  9. package/dist/commands/notes/repo/write.d.ts +24 -0
  10. package/dist/commands/notes/repo/write.js +251 -0
  11. package/dist/commands/notes/repo.d.ts +7 -186
  12. package/dist/commands/notes/repo.js +7 -1360
  13. package/dist/commands/schema/commandMetadata.js +4 -4
  14. package/dist/commands/schema/index.d.ts +1 -0
  15. package/dist/commands/schema/index.js +23 -2
  16. package/dist/commands/schema/renderSkills.d.ts +4 -0
  17. package/dist/commands/schema/renderSkills.js +41 -2
  18. package/dist/commands/skills/index.d.ts +26 -0
  19. package/dist/commands/skills/index.js +115 -11
  20. package/dist/commands/storage/repo/configs.d.ts +30 -0
  21. package/dist/commands/storage/repo/configs.js +259 -0
  22. package/dist/commands/storage/repo/operations.d.ts +25 -0
  23. package/dist/commands/storage/repo/operations.js +337 -0
  24. package/dist/commands/storage/repo/remote.d.ts +32 -0
  25. package/dist/commands/storage/repo/remote.js +132 -0
  26. package/dist/commands/storage/repo/resources.d.ts +12 -0
  27. package/dist/commands/storage/repo/resources.js +134 -0
  28. package/dist/commands/storage/repo/shared.d.ts +181 -0
  29. package/dist/commands/storage/repo/shared.js +36 -0
  30. package/dist/commands/storage/repo/targets.d.ts +12 -0
  31. package/dist/commands/storage/repo/targets.js +178 -0
  32. package/dist/commands/storage/repo.d.ts +3 -166
  33. package/dist/commands/storage/repo.js +4 -1034
  34. package/dist/contract/snapshot.d.ts +30 -0
  35. package/dist/contract/snapshot.js +114 -0
  36. package/dist/daemon/client.js +2 -2
  37. package/dist/powersync/uploader.js +27 -2
  38. package/dist/skills/commandRefs.d.ts +21 -0
  39. package/dist/skills/commandRefs.js +132 -0
  40. package/dist/skills/generate.d.ts +13 -0
  41. package/dist/skills/generate.js +80 -0
  42. package/dist/skills/registry.d.ts +13 -0
  43. package/dist/skills/registry.js +58 -0
  44. package/dist/utils/errorCodes.d.ts +311 -0
  45. package/dist/utils/errorCodes.js +92 -0
  46. package/dist/utils/errors.d.ts +13 -2
  47. package/dist/utils/errors.js +11 -0
  48. package/package.json +11 -4
@@ -0,0 +1,30 @@
1
+ export type ContractOption = {
2
+ name: string;
3
+ required: boolean;
4
+ type?: string;
5
+ enum?: string[];
6
+ };
7
+ export type ContractCommand = {
8
+ path: string;
9
+ command: string;
10
+ risk: string;
11
+ auth: string;
12
+ arguments: Array<{
13
+ name: string;
14
+ required: boolean;
15
+ }>;
16
+ options: ContractOption[];
17
+ outputFields: string[];
18
+ outputMetaFields: string[];
19
+ };
20
+ export type CliContractSnapshot = {
21
+ contractVersion: 1;
22
+ cliVersion: string;
23
+ commands: ContractCommand[];
24
+ errorCodes: Record<string, number>;
25
+ };
26
+ export declare function buildContractSnapshot(cliVersion: string): CliContractSnapshot;
27
+ export declare function renderContractSnapshot(snapshot: CliContractSnapshot): string;
28
+ /** Changes that can break an existing agent or script that relied on the previous contract. */
29
+ export declare function findBreakingChanges(previous: CliContractSnapshot, next: CliContractSnapshot): string[];
30
+ export declare function majorVersion(version: string): number;
@@ -0,0 +1,114 @@
1
+ // Machine-readable snapshot of the agent-facing CLI contract: command paths, arguments,
2
+ // options, risk/auth, structured output fields, and error codes with exit statuses.
3
+ // The committed copy (contract/cli-contract.json) makes every contract change visible in
4
+ // review, and `findBreakingChanges` refuses breaking changes without a major version bump.
5
+ import { listCommandSchemas } from '../commands/schema/spec.js';
6
+ import { ERROR_CODES } from '../utils/errorCodes.js';
7
+ const RISK_ORDER = ['read', 'write', 'high-risk-write'];
8
+ function flagOf(optionName) {
9
+ return optionName.match(/--[a-z0-9][a-z0-9-]*/i)?.[0] ?? optionName;
10
+ }
11
+ export function buildContractSnapshot(cliVersion) {
12
+ const commands = listCommandSchemas()
13
+ .map((schema) => ({
14
+ path: schema.path,
15
+ command: schema.command,
16
+ risk: schema.risk,
17
+ auth: schema.auth,
18
+ arguments: (schema.arguments ?? []).map((argument) => ({ name: argument.name, required: argument.required })),
19
+ options: (schema.options ?? [])
20
+ .map((option) => ({
21
+ name: option.name,
22
+ required: option.required === true,
23
+ ...(option.type ? { type: option.type } : {}),
24
+ ...(option.enum ? { enum: [...option.enum] } : {}),
25
+ }))
26
+ .sort((left, right) => flagOf(left.name).localeCompare(flagOf(right.name))),
27
+ outputFields: [...(schema.output.fields ?? [])],
28
+ outputMetaFields: [...(schema.output.metaFields ?? [])],
29
+ }))
30
+ .sort((left, right) => left.path.localeCompare(right.path));
31
+ const errorCodes = Object.fromEntries(Object.keys(ERROR_CODES).sort().map((code) => [code, ERROR_CODES[code].exitCode]));
32
+ return { contractVersion: 1, cliVersion, commands, errorCodes };
33
+ }
34
+ export function renderContractSnapshot(snapshot) {
35
+ return `${JSON.stringify(snapshot, null, 2)}\n`;
36
+ }
37
+ /** Changes that can break an existing agent or script that relied on the previous contract. */
38
+ export function findBreakingChanges(previous, next) {
39
+ const changes = [];
40
+ const nextCommands = new Map(next.commands.map((command) => [command.path, command]));
41
+ for (const before of previous.commands) {
42
+ const after = nextCommands.get(before.path);
43
+ if (!after) {
44
+ changes.push(`command ${before.path} was removed`);
45
+ continue;
46
+ }
47
+ if (after.command !== before.command) {
48
+ changes.push(`command ${before.path} changed invocation from \`${before.command}\` to \`${after.command}\``);
49
+ }
50
+ if (RISK_ORDER.indexOf(after.risk) > RISK_ORDER.indexOf(before.risk)) {
51
+ changes.push(`command ${before.path} risk escalated from ${before.risk} to ${after.risk}`);
52
+ }
53
+ if (before.auth !== 'required' && after.auth === 'required') {
54
+ changes.push(`command ${before.path} now requires auth`);
55
+ }
56
+ const requiredArgsBefore = before.arguments.filter((argument) => argument.required).length;
57
+ const requiredArgsAfter = after.arguments.filter((argument) => argument.required).length;
58
+ if (requiredArgsAfter > requiredArgsBefore) {
59
+ changes.push(`command ${before.path} gained a required positional argument`);
60
+ }
61
+ if (after.arguments.length < before.arguments.length) {
62
+ changes.push(`command ${before.path} lost a positional argument`);
63
+ }
64
+ const optionsAfter = new Map(after.options.map((option) => [flagOf(option.name), option]));
65
+ for (const option of before.options) {
66
+ const flag = flagOf(option.name);
67
+ const updated = optionsAfter.get(flag);
68
+ if (!updated) {
69
+ changes.push(`option ${before.path} ${flag} was removed`);
70
+ continue;
71
+ }
72
+ if (!option.required && updated.required) {
73
+ changes.push(`option ${before.path} ${flag} became required`);
74
+ }
75
+ if (option.type && updated.type && option.type !== updated.type) {
76
+ changes.push(`option ${before.path} ${flag} changed type from ${option.type} to ${updated.type}`);
77
+ }
78
+ for (const value of option.enum ?? []) {
79
+ if (updated.enum && !updated.enum.includes(value)) {
80
+ changes.push(`option ${before.path} ${flag} no longer accepts ${value}`);
81
+ }
82
+ }
83
+ }
84
+ for (const option of after.options) {
85
+ if (option.required && !before.options.some((previousOption) => flagOf(previousOption.name) === flagOf(option.name))) {
86
+ changes.push(`option ${before.path} ${flagOf(option.name)} was added as required`);
87
+ }
88
+ }
89
+ for (const field of before.outputFields) {
90
+ if (!after.outputFields.includes(field)) {
91
+ changes.push(`output field ${before.path}.${field} was removed`);
92
+ }
93
+ }
94
+ for (const field of before.outputMetaFields) {
95
+ if (!after.outputMetaFields.includes(field)) {
96
+ changes.push(`output meta field ${before.path}.${field} was removed`);
97
+ }
98
+ }
99
+ }
100
+ for (const [code, exitCode] of Object.entries(previous.errorCodes)) {
101
+ const updated = next.errorCodes[code];
102
+ if (updated === undefined) {
103
+ changes.push(`error code ${code} was removed`);
104
+ }
105
+ else if (updated !== exitCode) {
106
+ changes.push(`error code ${code} exit status changed from ${exitCode} to ${updated}`);
107
+ }
108
+ }
109
+ return changes;
110
+ }
111
+ export function majorVersion(version) {
112
+ const major = Number.parseInt(version.split('.')[0] ?? '', 10);
113
+ return Number.isFinite(major) ? major : 0;
114
+ }
@@ -2,7 +2,7 @@ import { request as httpRequest } from 'node:http';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { getDaemonSocketPath, isLegacyDaemonProcess, isProcessRunning, readPid, removePid, } from './pid.js';
4
4
  import { remainingDeadlineMs } from '../utils/deadline.js';
5
- import { DinoxError } from '../utils/errors.js';
5
+ import { DinoxError, wireErrorCode } from '../utils/errors.js';
6
6
  const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 10_000;
7
7
  const MAX_DAEMON_RESPONSE_BYTES = 5 * 1024 * 1024;
8
8
  function normalizedTimeoutMs(timeoutMs, fallbackMs) {
@@ -114,7 +114,7 @@ function toDaemonError(error) {
114
114
  if (isStructuredDaemonError(error)) {
115
115
  if (error.name === 'DinoxError' || error.code !== 'INTERNAL_ERROR') {
116
116
  return new DinoxError(error.message, {
117
- code: error.code,
117
+ code: wireErrorCode(error.code),
118
118
  details: error.details,
119
119
  suggestion: error.suggestion,
120
120
  suggestedAction: error.suggested_action,
@@ -3,6 +3,7 @@ import { UpdateType } from '@powersync/common';
3
3
  import { createTimeoutSignal, mergeAbortSignals } from '../utils/abort.js';
4
4
  import { toAuthorizationHeaderValue } from '../utils/authToken.js';
5
5
  import { sanitizeForTerminal } from '../utils/terminalSanitization.js';
6
+ import { nowIsoUtc } from '../utils/time.js';
6
7
  /**
7
8
  * Uploads each PowerSync CRUD transaction as ONE request to the backend v6 batch
8
9
  * endpoint (same contract as dinox-electron / dinox-web / dinox-app). The backend
@@ -23,6 +24,30 @@ const UPLOAD_REQUIRED_INSERT_DEFAULTS_BY_TABLE = new Map([
23
24
  // PowerSync omits NULL values from PUT payloads, while the backend requires these.
24
25
  ['c_note_template', { content: '', extra_data: '{}' }],
25
26
  ]);
27
+ /**
28
+ * CLI 1.x kept star state in `c_note.is_starred`; 2.x, the other clients, and the v6
29
+ * backend use `starred_at`. 1.x operations still queued in the same local database
30
+ * survive the upgrade, and the backend rejects the unknown column, which blocks every
31
+ * later upload. Translate them the same way 2.x writes a star change.
32
+ */
33
+ function translateLegacyNoteStar(table, values) {
34
+ if (table !== 'c_note' || !Object.prototype.hasOwnProperty.call(values, 'is_starred'))
35
+ return values;
36
+ const { is_starred: legacyStarred, ...rest } = values;
37
+ if (!Object.prototype.hasOwnProperty.call(rest, 'starred_at')) {
38
+ const starred = legacyStarred === 1 || legacyStarred === true || legacyStarred === '1';
39
+ rest.starred_at = starred
40
+ ? (typeof rest.updated_at === 'string' && rest.updated_at ? rest.updated_at : nowIsoUtc())
41
+ : null;
42
+ }
43
+ return rest;
44
+ }
45
+ function dropLegacyColumns(table, values) {
46
+ if (!values || table !== 'c_note' || !('is_starred' in values))
47
+ return values;
48
+ const { is_starred: _legacyStarred, ...rest } = values;
49
+ return rest;
50
+ }
26
51
  function isRecord(value) {
27
52
  return value !== null && typeof value === 'object' && !Array.isArray(value);
28
53
  }
@@ -94,9 +119,9 @@ export function toUploadOperation(op) {
94
119
  type: table,
95
120
  id: String(op.id),
96
121
  ...(op.transactionId !== undefined ? { tx_id: op.transactionId } : {}),
97
- ...(data ? { data: omitColumns(table, data) } : {}),
122
+ ...(data ? { data: omitColumns(table, translateLegacyNoteStar(table, data)) } : {}),
98
123
  // DELETE needs no base: deletes win (PowerSync default), nothing to compare.
99
- ...(name === 'PATCH' ? compactPreviousValues(op.previousValues) : {}),
124
+ ...(name === 'PATCH' ? compactPreviousValues(dropLegacyColumns(table, op.previousValues)) : {}),
100
125
  ...(op.metadata !== undefined ? { metadata: op.metadata } : {}),
101
126
  };
102
127
  }
@@ -0,0 +1,21 @@
1
+ import { type CommandSchema } from '../commands/schema/spec.js';
2
+ export type SkillCommandReferenceIssue = {
3
+ /** Path relative to the skills directory. */
4
+ file: string;
5
+ line: number;
6
+ snippet: string;
7
+ message: string;
8
+ };
9
+ type CommandIndex = {
10
+ byCommand: Map<string, CommandSchema>;
11
+ groups: Set<string>;
12
+ };
13
+ /** Extracts `dino ...` invocations from inline code spans and fenced code block lines. */
14
+ export declare function extractDinoInvocations(markdown: string): Array<{
15
+ line: number;
16
+ text: string;
17
+ }>;
18
+ export declare function validateDinoInvocation(text: string, index?: CommandIndex): string | null;
19
+ /** Verifies that every `dino ...` invocation in the skill markdown resolves against the command schemas. */
20
+ export declare function checkSkillCommandReferences(skillsDir: string): SkillCommandReferenceIssue[];
21
+ export {};
@@ -0,0 +1,132 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { listCommandSchemas } from '../commands/schema/spec.js';
4
+ // Global flags accepted by every command (see src/cli.ts) plus Commander builtins.
5
+ const GLOBAL_FLAGS = new Set([
6
+ '--json',
7
+ '--format',
8
+ '--offline',
9
+ '--require-sync',
10
+ '--sync-timeout',
11
+ '--verbose',
12
+ '--help',
13
+ '--version',
14
+ ]);
15
+ const SHELL_BOUNDARY = /\s(?:\||&&|\|\||;|>|2>|#)\s/;
16
+ function buildCommandIndex() {
17
+ const byCommand = new Map();
18
+ const groups = new Set();
19
+ for (const schema of listCommandSchemas()) {
20
+ const words = schema.command.split(/\s+/).slice(1);
21
+ byCommand.set(words.join(' '), schema);
22
+ if (words.length > 1) {
23
+ groups.add(words[0]);
24
+ }
25
+ }
26
+ return { byCommand, groups };
27
+ }
28
+ function optionFlags(schema) {
29
+ const flags = new Set();
30
+ for (const option of schema.options ?? []) {
31
+ for (const match of option.name.matchAll(/--[a-z0-9][a-z0-9-]*/gi)) {
32
+ flags.add(match[0]);
33
+ }
34
+ }
35
+ return flags;
36
+ }
37
+ /** Extracts `dino ...` invocations from inline code spans and fenced code block lines. */
38
+ export function extractDinoInvocations(markdown) {
39
+ const invocations = [];
40
+ const lines = markdown.split(/\r?\n/);
41
+ let inFence = false;
42
+ lines.forEach((rawLine, index) => {
43
+ const lineNumber = index + 1;
44
+ if (/^\s*(```|~~~)/.test(rawLine)) {
45
+ inFence = !inFence;
46
+ return;
47
+ }
48
+ if (inFence) {
49
+ const match = rawLine.match(/(?:^|\s|\$\s*)(dino\s+[^`]*)$/);
50
+ if (match?.[1]) {
51
+ invocations.push({ line: lineNumber, text: match[1] });
52
+ }
53
+ return;
54
+ }
55
+ for (const match of rawLine.matchAll(/`(dino\s+[^`]+)`/g)) {
56
+ if (match[1]) {
57
+ invocations.push({ line: lineNumber, text: match[1] });
58
+ }
59
+ }
60
+ });
61
+ return invocations;
62
+ }
63
+ export function validateDinoInvocation(text, index = buildCommandIndex()) {
64
+ const command = text.split(SHELL_BOUNDARY)[0]?.trim() ?? '';
65
+ const tokens = command.split(/\s+/).slice(1);
66
+ const first = tokens[0];
67
+ // Placeholders such as `dino <command>` or `dino ...` are prose, not invocations.
68
+ if (!first || !/^[a-z][a-z0-9-]*$/.test(first)) {
69
+ return null;
70
+ }
71
+ let schema;
72
+ let consumed = 0;
73
+ for (let length = Math.min(tokens.length, 3); length >= 1; length -= 1) {
74
+ const candidate = index.byCommand.get(tokens.slice(0, length).join(' '));
75
+ if (candidate) {
76
+ schema = candidate;
77
+ consumed = length;
78
+ break;
79
+ }
80
+ }
81
+ if (!schema) {
82
+ const second = tokens[1];
83
+ if (index.groups.has(first) && (!second || !/^[a-z][a-z0-9-]*$/.test(second))) {
84
+ return null;
85
+ }
86
+ return `unknown command \`dino ${tokens.slice(0, index.groups.has(first) ? 2 : 1).join(' ')}\``;
87
+ }
88
+ const known = optionFlags(schema);
89
+ for (const token of tokens.slice(consumed)) {
90
+ const flag = token.match(/^(--[a-z0-9][a-z0-9-]*)/i)?.[1];
91
+ if (!flag || GLOBAL_FLAGS.has(flag) || known.has(flag)) {
92
+ continue;
93
+ }
94
+ return `\`${schema.command}\` has no option \`${flag}\``;
95
+ }
96
+ return null;
97
+ }
98
+ function listMarkdownFiles(dir) {
99
+ const files = [];
100
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
101
+ if (entry.name.startsWith('.'))
102
+ continue;
103
+ const fullPath = path.join(dir, entry.name);
104
+ if (entry.isDirectory()) {
105
+ files.push(...listMarkdownFiles(fullPath));
106
+ }
107
+ else if (entry.name.endsWith('.md')) {
108
+ files.push(fullPath);
109
+ }
110
+ }
111
+ return files.sort();
112
+ }
113
+ /** Verifies that every `dino ...` invocation in the skill markdown resolves against the command schemas. */
114
+ export function checkSkillCommandReferences(skillsDir) {
115
+ const index = buildCommandIndex();
116
+ const issues = [];
117
+ for (const filePath of listMarkdownFiles(skillsDir)) {
118
+ const markdown = fs.readFileSync(filePath, 'utf8');
119
+ for (const invocation of extractDinoInvocations(markdown)) {
120
+ const message = validateDinoInvocation(invocation.text, index);
121
+ if (message) {
122
+ issues.push({
123
+ file: path.relative(skillsDir, filePath).split(path.sep).join('/'),
124
+ line: invocation.line,
125
+ snippet: invocation.text.trim(),
126
+ message,
127
+ });
128
+ }
129
+ }
130
+ }
131
+ return issues;
132
+ }
@@ -0,0 +1,13 @@
1
+ import { type GeneratedSkillSection } from './registry.js';
2
+ export type GeneratedSkillFile = {
3
+ skill: string;
4
+ /** Path relative to the skills directory. */
5
+ file: string;
6
+ marker: GeneratedSkillSection['marker'] | 'PORTABLE_RUNTIME';
7
+ render: () => string;
8
+ };
9
+ /** Every generated block in the published skill set, derived from the skill registry. */
10
+ export declare function listGeneratedSkillFiles(): GeneratedSkillFile[];
11
+ export declare function syncGeneratedSkills(skillsDir: string): void;
12
+ /** Returns human-readable issues; empty when every generated block is current. */
13
+ export declare function checkGeneratedSkills(skillsDir: string): string[];
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { listGeneratedCommandSectionKeys, renderBundledSkillReference, renderErrorCodeReference, renderGeneratedCommandSection, renderPortableRuntimeContract, replaceGeneratedSection, } from '../commands/schema/renderSkills.js';
4
+ import { SKILL_REGISTRY } from './registry.js';
5
+ /** Every generated block in the published skill set, derived from the skill registry. */
6
+ export function listGeneratedSkillFiles() {
7
+ const files = [];
8
+ for (const entry of SKILL_REGISTRY) {
9
+ files.push({
10
+ skill: entry.name,
11
+ file: `${entry.name}/SKILL.md`,
12
+ marker: 'PORTABLE_RUNTIME',
13
+ render: renderPortableRuntimeContract,
14
+ });
15
+ for (const section of entry.generated) {
16
+ const key = section.key;
17
+ if (section.marker === 'GENERATED_COMMANDS' && !key) {
18
+ throw new Error(`${entry.name}/${section.file} GENERATED_COMMANDS section is missing a key`);
19
+ }
20
+ files.push({
21
+ skill: entry.name,
22
+ file: `${entry.name}/${section.file}`,
23
+ marker: section.marker,
24
+ render: section.marker === 'GENERATED_REFERENCE'
25
+ ? renderBundledSkillReference
26
+ : section.marker === 'GENERATED_ERRORS'
27
+ ? renderErrorCodeReference
28
+ : () => renderGeneratedCommandSection(key),
29
+ });
30
+ }
31
+ }
32
+ return files;
33
+ }
34
+ function applyGeneratedBlock(skillsDir, generated) {
35
+ const filePath = path.join(skillsDir, generated.file);
36
+ const current = fs.readFileSync(filePath, 'utf8');
37
+ return { current, next: replaceGeneratedSection(current, generated.marker, generated.render()) };
38
+ }
39
+ export function syncGeneratedSkills(skillsDir) {
40
+ for (const generated of listGeneratedSkillFiles()) {
41
+ const { current, next } = applyGeneratedBlock(skillsDir, generated);
42
+ if (next !== current) {
43
+ fs.writeFileSync(path.join(skillsDir, generated.file), next);
44
+ }
45
+ }
46
+ }
47
+ /** Returns human-readable issues; empty when every generated block is current. */
48
+ export function checkGeneratedSkills(skillsDir) {
49
+ const issues = [];
50
+ const usedKeys = new Set();
51
+ for (const entry of SKILL_REGISTRY) {
52
+ for (const section of entry.generated) {
53
+ if (section.key) {
54
+ if (usedKeys.has(section.key)) {
55
+ issues.push(`generated section key ${section.key} is used more than once`);
56
+ }
57
+ usedKeys.add(section.key);
58
+ }
59
+ }
60
+ }
61
+ for (const key of listGeneratedCommandSectionKeys()) {
62
+ if (!usedKeys.has(key)) {
63
+ issues.push(`generated section ${key} is configured but not registered for any skill file`);
64
+ }
65
+ }
66
+ for (const generated of listGeneratedSkillFiles()) {
67
+ let result;
68
+ try {
69
+ result = applyGeneratedBlock(skillsDir, generated);
70
+ }
71
+ catch (error) {
72
+ issues.push(`${generated.file}: ${error instanceof Error ? error.message : String(error)}`);
73
+ continue;
74
+ }
75
+ if (result.current !== result.next) {
76
+ issues.push(`${generated.file} ${generated.marker} block is stale; run pnpm skills:sync`);
77
+ }
78
+ }
79
+ return issues;
80
+ }
@@ -0,0 +1,13 @@
1
+ export type GeneratedSkillSection = Readonly<{
2
+ /** File path relative to the skill directory. */
3
+ file: string;
4
+ marker: 'GENERATED_COMMANDS' | 'GENERATED_REFERENCE' | 'GENERATED_ERRORS';
5
+ /** Section key in renderSkills; required for GENERATED_COMMANDS. */
6
+ key?: string;
7
+ }>;
8
+ export type SkillRegistryEntry = Readonly<{
9
+ name: string;
10
+ generated: readonly GeneratedSkillSection[];
11
+ }>;
12
+ export declare const SKILL_REGISTRY: readonly SkillRegistryEntry[];
13
+ export declare function listRegisteredSkillNames(): string[];
@@ -0,0 +1,58 @@
1
+ // Single source of truth for the published Dinox skill set. Generation
2
+ // (`pnpm skills:sync`), validation (`pnpm skills:check`), tests, and the
3
+ // collection manifest all derive their skill lists from this registry.
4
+ function commands(file, key) {
5
+ return Object.freeze({ file, marker: 'GENERATED_COMMANDS', key });
6
+ }
7
+ const REGISTRY_ENTRIES = [
8
+ { name: 'dino-auth', generated: [] },
9
+ { name: 'dino-config', generated: [] },
10
+ {
11
+ name: 'dino-dinox',
12
+ generated: [
13
+ { file: 'references/commands.md', marker: 'GENERATED_REFERENCE' },
14
+ { file: 'references/errors.md', marker: 'GENERATED_ERRORS' },
15
+ ],
16
+ },
17
+ {
18
+ name: 'dino-manage-boxes',
19
+ generated: [commands('SKILL.md', 'skill:dino-manage-boxes')],
20
+ },
21
+ {
22
+ name: 'dino-manage-prompts',
23
+ generated: [commands('SKILL.md', 'skill:dino-manage-prompts')],
24
+ },
25
+ {
26
+ name: 'dino-manage-tags',
27
+ generated: [commands('SKILL.md', 'skill:dino-manage-tags')],
28
+ },
29
+ {
30
+ name: 'dino-manage-todo',
31
+ generated: [
32
+ commands('references/todo-search.md', 'ref:dino-manage-todo:todo-search'),
33
+ commands('references/todo-mutations.md', 'ref:dino-manage-todo:todo-mutations'),
34
+ ],
35
+ },
36
+ {
37
+ name: 'dino-manage-views',
38
+ generated: [commands('SKILL.md', 'skill:dino-manage-views')],
39
+ },
40
+ {
41
+ name: 'dino-note',
42
+ generated: [
43
+ commands('references/search-and-read.md', 'ref:dino-note:search-and-read'),
44
+ commands('references/create.md', 'ref:dino-note:create'),
45
+ commands('references/update-and-delete.md', 'ref:dino-note:update-and-delete'),
46
+ ],
47
+ },
48
+ {
49
+ name: 'dino-storage',
50
+ generated: [commands('SKILL.md', 'skill:dino-storage')],
51
+ },
52
+ { name: 'dino-sync', generated: [] },
53
+ { name: 'dino-update-cli', generated: [] },
54
+ ];
55
+ export const SKILL_REGISTRY = Object.freeze(REGISTRY_ENTRIES.map((entry) => Object.freeze({ ...entry, generated: Object.freeze([...entry.generated]) })));
56
+ export function listRegisteredSkillNames() {
57
+ return SKILL_REGISTRY.map((entry) => entry.name);
58
+ }