dryai 0.2.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.
@@ -0,0 +1,192 @@
1
+ import { z } from 'zod';
2
+ import type { AgentsContext } from './context.js';
3
+ declare const skillLockEntrySchema: z.ZodObject<{
4
+ name: z.ZodString;
5
+ repo: z.ZodString;
6
+ path: z.ZodString;
7
+ ref: z.ZodOptional<z.ZodString>;
8
+ commit: z.ZodString;
9
+ importedAt: z.ZodString;
10
+ updatedAt: z.ZodString;
11
+ }, "strip", z.ZodTypeAny, {
12
+ name: string;
13
+ repo: string;
14
+ path: string;
15
+ commit: string;
16
+ importedAt: string;
17
+ updatedAt: string;
18
+ ref?: string | undefined;
19
+ }, {
20
+ name: string;
21
+ repo: string;
22
+ path: string;
23
+ commit: string;
24
+ importedAt: string;
25
+ updatedAt: string;
26
+ ref?: string | undefined;
27
+ }>;
28
+ declare const skillsLockfileSchema: z.ZodEffects<z.ZodObject<{
29
+ version: z.ZodLiteral<1>;
30
+ skills: z.ZodArray<z.ZodObject<{
31
+ name: z.ZodString;
32
+ repo: z.ZodString;
33
+ path: z.ZodString;
34
+ ref: z.ZodOptional<z.ZodString>;
35
+ commit: z.ZodString;
36
+ importedAt: z.ZodString;
37
+ updatedAt: z.ZodString;
38
+ }, "strip", z.ZodTypeAny, {
39
+ name: string;
40
+ repo: string;
41
+ path: string;
42
+ commit: string;
43
+ importedAt: string;
44
+ updatedAt: string;
45
+ ref?: string | undefined;
46
+ }, {
47
+ name: string;
48
+ repo: string;
49
+ path: string;
50
+ commit: string;
51
+ importedAt: string;
52
+ updatedAt: string;
53
+ ref?: string | undefined;
54
+ }>, "many">;
55
+ }, "strip", z.ZodTypeAny, {
56
+ version: 1;
57
+ skills: {
58
+ name: string;
59
+ repo: string;
60
+ path: string;
61
+ commit: string;
62
+ importedAt: string;
63
+ updatedAt: string;
64
+ ref?: string | undefined;
65
+ }[];
66
+ }, {
67
+ version: 1;
68
+ skills: {
69
+ name: string;
70
+ repo: string;
71
+ path: string;
72
+ commit: string;
73
+ importedAt: string;
74
+ updatedAt: string;
75
+ ref?: string | undefined;
76
+ }[];
77
+ }>, {
78
+ version: 1;
79
+ skills: {
80
+ name: string;
81
+ repo: string;
82
+ path: string;
83
+ commit: string;
84
+ importedAt: string;
85
+ updatedAt: string;
86
+ ref?: string | undefined;
87
+ }[];
88
+ }, {
89
+ version: 1;
90
+ skills: {
91
+ name: string;
92
+ repo: string;
93
+ path: string;
94
+ commit: string;
95
+ importedAt: string;
96
+ updatedAt: string;
97
+ ref?: string | undefined;
98
+ }[];
99
+ }>;
100
+ export type ManagedSkill = z.infer<typeof skillLockEntrySchema>;
101
+ export type SkillsLockfile = z.infer<typeof skillsLockfileSchema>;
102
+ export type RemoteSkillSnapshot = {
103
+ cleanup: () => Promise<void>;
104
+ commit: string;
105
+ sourceDir: string;
106
+ };
107
+ export type RemoteRepoCheckout = {
108
+ checkoutDir: string;
109
+ cleanup: () => Promise<void>;
110
+ commit: string;
111
+ };
112
+ export declare function createEmptySkillsLockfile(): SkillsLockfile;
113
+ export declare function ensureSkillsRoot(context: AgentsContext): Promise<void>;
114
+ export declare function ensureSkillsLockfile(context: AgentsContext): Promise<void>;
115
+ export declare function loadSkillsLockfile(context: AgentsContext): Promise<SkillsLockfile>;
116
+ export declare function saveSkillsLockfile(context: AgentsContext, { lockfile }: {
117
+ lockfile: SkillsLockfile;
118
+ }): Promise<void>;
119
+ export declare function findManagedSkill(lockfile: SkillsLockfile, { name }: {
120
+ name: string;
121
+ }): ManagedSkill | undefined;
122
+ export declare function upsertManagedSkill(lockfile: SkillsLockfile, { updatedSkill }: {
123
+ updatedSkill: ManagedSkill;
124
+ }): SkillsLockfile;
125
+ export declare function removeManagedSkill(lockfile: SkillsLockfile, { name }: {
126
+ name: string;
127
+ }): SkillsLockfile;
128
+ export declare function listLocalSkillDirectories(context: AgentsContext): Promise<string[]>;
129
+ export declare function getManagedSkillDirectory(context: AgentsContext, { skillName }: {
130
+ skillName: string;
131
+ }): string;
132
+ export declare function deriveSkillName({ repo, skillPath, explicitName, }: {
133
+ repo: string;
134
+ skillPath: string;
135
+ explicitName: string | undefined;
136
+ }): string;
137
+ export declare function normalizeRemoteRepo(repo: string): string;
138
+ /**
139
+ * Builds the canonical remote path for a managed skill under the repository `skills/` directory.
140
+ */
141
+ export declare function resolveManagedSkillImportPath({ skillName, }: {
142
+ skillName: string;
143
+ }): string;
144
+ export declare function normalizeImportedSkillPath(skillPath: string | undefined): string;
145
+ export declare function createImportedSkillRecord(input: {
146
+ commit: string;
147
+ importedAt: string;
148
+ name: string;
149
+ path: string;
150
+ ref: string | undefined;
151
+ repo: string;
152
+ }): ManagedSkill;
153
+ export declare function createUpdatedSkillRecord(input: {
154
+ commit: string;
155
+ existingSkill: ManagedSkill;
156
+ updatedAt: string;
157
+ }): ManagedSkill;
158
+ /**
159
+ * Clones a remote repository into a temporary checkout and resolves the fetched commit.
160
+ */
161
+ export declare function cloneRemoteRepo(input: {
162
+ ref: string | undefined;
163
+ repo: string;
164
+ }): Promise<RemoteRepoCheckout>;
165
+ /**
166
+ * Resolves and validates a managed skill directory from a temporary repository checkout.
167
+ */
168
+ export declare function resolveSkillSourceDir(input: {
169
+ checkoutDir: string;
170
+ repo: string;
171
+ skillName: string;
172
+ }): Promise<string>;
173
+ /**
174
+ * Fetches a validated remote skill directory snapshot for a specific repository path.
175
+ */
176
+ export declare function fetchRemoteSkillSnapshot(input: {
177
+ ref: string | undefined;
178
+ repo: string;
179
+ skillPath: string;
180
+ }): Promise<RemoteSkillSnapshot>;
181
+ export declare function replaceManagedSkillDirectory({ targetDir, sourceDir, }: {
182
+ targetDir: string;
183
+ sourceDir: string;
184
+ }): Promise<void>;
185
+ export declare function removeManagedSkillDirectory(context: AgentsContext, { skillName }: {
186
+ skillName: string;
187
+ }): Promise<void>;
188
+ export declare function formatManagedSkillSummary(skill: ManagedSkill): string;
189
+ export declare function shortCommit(commit: string): string;
190
+ export declare function timestampNow(): string;
191
+ export {};
192
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1,329 @@
1
+ import fs from 'fs-extra';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { simpleGit } from 'simple-git';
5
+ import { z } from 'zod';
6
+ const skillLockEntrySchema = z.object({
7
+ name: z.string().min(1),
8
+ repo: z.string().min(1),
9
+ path: z.string().min(1),
10
+ ref: z.string().min(1).optional(),
11
+ commit: z.string().min(1),
12
+ importedAt: z.string().datetime({ offset: true }),
13
+ updatedAt: z.string().datetime({ offset: true }),
14
+ });
15
+ const skillsLockfileSchema = z
16
+ .object({
17
+ version: z.literal(1),
18
+ skills: z.array(skillLockEntrySchema),
19
+ })
20
+ .superRefine((lockfile, refinementContext) => {
21
+ const seenNames = new Set();
22
+ lockfile.skills.forEach((skill, index) => {
23
+ if (seenNames.has(skill.name)) {
24
+ refinementContext.addIssue({
25
+ code: z.ZodIssueCode.custom,
26
+ message: `Duplicate managed skill name: ${skill.name}`,
27
+ path: ['skills', index, 'name'],
28
+ });
29
+ return;
30
+ }
31
+ seenNames.add(skill.name);
32
+ });
33
+ });
34
+ export function createEmptySkillsLockfile() {
35
+ return {
36
+ version: 1,
37
+ skills: [],
38
+ };
39
+ }
40
+ export async function ensureSkillsRoot(context) {
41
+ await fs.ensureDir(context.sourceRoots.skills);
42
+ }
43
+ export async function ensureSkillsLockfile(context) {
44
+ if (await fs.pathExists(context.skillsLockfilePath)) {
45
+ return;
46
+ }
47
+ await saveSkillsLockfile(context, { lockfile: createEmptySkillsLockfile() });
48
+ }
49
+ export async function loadSkillsLockfile(context) {
50
+ if (!(await fs.pathExists(context.skillsLockfilePath))) {
51
+ return createEmptySkillsLockfile();
52
+ }
53
+ const rawLockfile = await fs.readFile(context.skillsLockfilePath, 'utf8');
54
+ const parsedJson = JSON.parse(rawLockfile);
55
+ const parsedLockfile = skillsLockfileSchema.safeParse(parsedJson);
56
+ if (parsedLockfile.success) {
57
+ return sortSkillsLockfile(parsedLockfile.data);
58
+ }
59
+ const issues = parsedLockfile.error.issues
60
+ .map((issue) => {
61
+ const issuePath = issue.path.length > 0 ? issue.path.join('.') : 'root';
62
+ return `- ${issuePath}: ${issue.message}`;
63
+ })
64
+ .join('\n');
65
+ throw new Error(`Invalid skills lockfile at ${context.skillsLockfilePath}:\n${issues}`);
66
+ }
67
+ export async function saveSkillsLockfile(context, { lockfile }) {
68
+ const normalizedLockfile = sortSkillsLockfile(lockfile);
69
+ await fs.writeFile(context.skillsLockfilePath, `${JSON.stringify(normalizedLockfile, null, 2)}\n`, 'utf8');
70
+ }
71
+ export function findManagedSkill(lockfile, { name }) {
72
+ return lockfile.skills.find((skill) => skill.name === name);
73
+ }
74
+ export function upsertManagedSkill(lockfile, { updatedSkill }) {
75
+ const remainingSkills = lockfile.skills.filter((skill) => skill.name !== updatedSkill.name);
76
+ return sortSkillsLockfile({
77
+ version: lockfile.version,
78
+ skills: [...remainingSkills, updatedSkill],
79
+ });
80
+ }
81
+ export function removeManagedSkill(lockfile, { name }) {
82
+ return sortSkillsLockfile({
83
+ version: lockfile.version,
84
+ skills: lockfile.skills.filter((skill) => skill.name !== name),
85
+ });
86
+ }
87
+ export async function listLocalSkillDirectories(context) {
88
+ await ensureSkillsRoot(context);
89
+ const entries = await fs.readdir(context.sourceRoots.skills, {
90
+ withFileTypes: true,
91
+ });
92
+ return entries
93
+ .filter((entry) => entry.isDirectory())
94
+ .map((entry) => entry.name)
95
+ .sort((left, right) => left.localeCompare(right));
96
+ }
97
+ export function getManagedSkillDirectory(context, { skillName }) {
98
+ return path.join(context.sourceRoots.skills, skillName);
99
+ }
100
+ export function deriveSkillName({ repo, skillPath, explicitName, }) {
101
+ const candidateName = explicitName ?? inferDefaultSkillName({ repo, skillPath });
102
+ if (candidateName.length === 0 ||
103
+ candidateName === '.' ||
104
+ candidateName === '..' ||
105
+ candidateName.includes('/') ||
106
+ candidateName.includes('\\')) {
107
+ throw new Error(`Invalid skill name: ${candidateName}`);
108
+ }
109
+ return candidateName;
110
+ }
111
+ export function normalizeRemoteRepo(repo) {
112
+ const trimmedRepo = repo.trim();
113
+ if (trimmedRepo.length === 0) {
114
+ throw new Error('Repository may not be empty');
115
+ }
116
+ if (githubRepoShorthandPattern.test(trimmedRepo)) {
117
+ const [owner, rawRepoName] = trimmedRepo.split('/');
118
+ const repoName = rawRepoName.endsWith('.git')
119
+ ? rawRepoName.slice(0, -4)
120
+ : rawRepoName;
121
+ return `https://github.com/${owner}/${repoName}.git`;
122
+ }
123
+ return trimmedRepo;
124
+ }
125
+ /**
126
+ * Builds the canonical remote path for a managed skill under the repository `skills/` directory.
127
+ */
128
+ export function resolveManagedSkillImportPath({ skillName, }) {
129
+ const trimmedSkillName = skillName.trim();
130
+ if (trimmedSkillName.length === 0 ||
131
+ trimmedSkillName === '.' ||
132
+ trimmedSkillName === '..' ||
133
+ trimmedSkillName.includes('/') ||
134
+ trimmedSkillName.includes('\\')) {
135
+ throw new Error(`Invalid skill name: ${skillName}`);
136
+ }
137
+ return `skills/${trimmedSkillName}`;
138
+ }
139
+ export function normalizeImportedSkillPath(skillPath) {
140
+ const normalizedPath = skillPath && skillPath.length > 0 ? skillPath : '.';
141
+ return path.normalize(normalizedPath);
142
+ }
143
+ export function createImportedSkillRecord(input) {
144
+ return {
145
+ commit: input.commit,
146
+ importedAt: input.importedAt,
147
+ name: input.name,
148
+ path: input.path,
149
+ ref: input.ref,
150
+ repo: input.repo,
151
+ updatedAt: input.importedAt,
152
+ };
153
+ }
154
+ export function createUpdatedSkillRecord(input) {
155
+ return {
156
+ ...input.existingSkill,
157
+ commit: input.commit,
158
+ updatedAt: input.updatedAt,
159
+ };
160
+ }
161
+ /**
162
+ * Clones a remote repository into a temporary checkout and resolves the fetched commit.
163
+ */
164
+ export async function cloneRemoteRepo(input) {
165
+ const normalizedRepo = normalizeRemoteRepo(input.repo);
166
+ const checkoutDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agents-skill.'));
167
+ try {
168
+ const git = simpleGit(checkoutDir);
169
+ await git.init();
170
+ await git.addRemote('origin', normalizedRepo);
171
+ if (input.ref) {
172
+ await git.fetch('origin', input.ref, ['--depth', '1']);
173
+ }
174
+ else {
175
+ await git.fetch('origin', 'HEAD', ['--depth', '1']);
176
+ }
177
+ await git.checkout(['--quiet', 'FETCH_HEAD']);
178
+ return {
179
+ checkoutDir,
180
+ cleanup: async () => {
181
+ await fs.remove(checkoutDir);
182
+ },
183
+ commit: await git.revparse(['HEAD']),
184
+ };
185
+ }
186
+ catch (error) {
187
+ await fs.remove(checkoutDir);
188
+ throw toError({
189
+ prefix: `Failed to fetch repository from ${normalizedRepo}`,
190
+ error,
191
+ });
192
+ }
193
+ }
194
+ /**
195
+ * Resolves and validates a managed skill directory from a temporary repository checkout.
196
+ */
197
+ export async function resolveSkillSourceDir(input) {
198
+ const skillPath = resolveManagedSkillImportPath({
199
+ skillName: input.skillName,
200
+ });
201
+ const sourceDir = resolveRemoteSkillDirectory({
202
+ checkoutDir: input.checkoutDir,
203
+ skillPath,
204
+ });
205
+ await validateRemoteSkillDirectory({
206
+ sourceDir,
207
+ skillPath,
208
+ repo: normalizeRemoteRepo(input.repo),
209
+ });
210
+ return sourceDir;
211
+ }
212
+ /**
213
+ * Fetches a validated remote skill directory snapshot for a specific repository path.
214
+ */
215
+ export async function fetchRemoteSkillSnapshot(input) {
216
+ const normalizedRepo = normalizeRemoteRepo(input.repo);
217
+ const checkout = await cloneRemoteRepo({
218
+ ref: input.ref,
219
+ repo: normalizedRepo,
220
+ });
221
+ try {
222
+ const sourceDir = resolveRemoteSkillDirectory({
223
+ checkoutDir: checkout.checkoutDir,
224
+ skillPath: input.skillPath,
225
+ });
226
+ await validateRemoteSkillDirectory({
227
+ sourceDir,
228
+ skillPath: input.skillPath,
229
+ repo: normalizedRepo,
230
+ });
231
+ return {
232
+ cleanup: checkout.cleanup,
233
+ commit: checkout.commit,
234
+ sourceDir,
235
+ };
236
+ }
237
+ catch (error) {
238
+ await checkout.cleanup();
239
+ throw toError({
240
+ prefix: `Failed to fetch skill from ${normalizedRepo}`,
241
+ error,
242
+ });
243
+ }
244
+ }
245
+ export async function replaceManagedSkillDirectory({ targetDir, sourceDir, }) {
246
+ await fs.ensureDir(path.dirname(targetDir));
247
+ const stagingRoot = await fs.mkdtemp(path.join(path.dirname(targetDir), `${path.basename(targetDir)}.`));
248
+ const stagedDir = path.join(stagingRoot, path.basename(targetDir));
249
+ try {
250
+ await fs.copy(sourceDir, stagedDir);
251
+ await fs.remove(targetDir);
252
+ await fs.move(stagedDir, targetDir, { overwrite: true });
253
+ }
254
+ finally {
255
+ await fs.remove(stagingRoot);
256
+ }
257
+ }
258
+ export async function removeManagedSkillDirectory(context, { skillName }) {
259
+ await fs.remove(getManagedSkillDirectory(context, { skillName }));
260
+ }
261
+ export function formatManagedSkillSummary(skill) {
262
+ const refLabel = skill.ref ? skill.ref : 'HEAD';
263
+ return `${skill.name} repo=${skill.repo} path=${skill.path} ref=${refLabel} commit=${shortCommit(skill.commit)}`;
264
+ }
265
+ export function shortCommit(commit) {
266
+ return commit.slice(0, 7);
267
+ }
268
+ export function timestampNow() {
269
+ return new Date().toISOString();
270
+ }
271
+ function sortSkillsLockfile(lockfile) {
272
+ return {
273
+ version: lockfile.version,
274
+ skills: [...lockfile.skills].sort((left, right) => left.name.localeCompare(right.name)),
275
+ };
276
+ }
277
+ /**
278
+ * Derives the default managed skill name when the caller does not provide `--as`.
279
+ *
280
+ * When `skillPath` points to a subdirectory, the last path segment is used.
281
+ * When `skillPath` is `.`, the repository name is used instead.
282
+ *
283
+ * @example
284
+ * Input: { repo: 'anthropics/skills', skillPath: 'skills/skill-creator' }
285
+ * Output: 'skill-creator'
286
+ *
287
+ * @example
288
+ * Input: { repo: 'https://github.com/anthropics/skills.git', skillPath: '.' }
289
+ * Output: 'skills'
290
+ */
291
+ function inferDefaultSkillName({ repo, skillPath, }) {
292
+ if (skillPath !== '.') {
293
+ return path.basename(skillPath);
294
+ }
295
+ const trimmedRepo = repo.replace(/\/+$/u, '');
296
+ const repoSegments = trimmedRepo.split(/[/:]/u).filter((segment) => segment);
297
+ const lastSegment = repoSegments[repoSegments.length - 1] ?? trimmedRepo;
298
+ return lastSegment.endsWith('.git') ? lastSegment.slice(0, -4) : lastSegment;
299
+ }
300
+ const githubRepoShorthandPattern = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/u;
301
+ function resolveRemoteSkillDirectory({ checkoutDir, skillPath, }) {
302
+ const candidateDir = path.resolve(checkoutDir, skillPath);
303
+ const relativeCandidatePath = path.relative(checkoutDir, candidateDir);
304
+ if (relativeCandidatePath === '..' ||
305
+ relativeCandidatePath.startsWith(`..${path.sep}`) ||
306
+ path.isAbsolute(relativeCandidatePath)) {
307
+ throw new Error(`Skill path escapes the repository checkout: ${skillPath}`);
308
+ }
309
+ return candidateDir;
310
+ }
311
+ async function validateRemoteSkillDirectory({ sourceDir, skillPath, repo, }) {
312
+ if (!(await fs.pathExists(sourceDir))) {
313
+ throw new Error(`Skill path does not exist in ${repo}: ${skillPath}`);
314
+ }
315
+ const stats = await fs.stat(sourceDir);
316
+ if (!stats.isDirectory()) {
317
+ throw new Error(`Skill path is not a directory in ${repo}: ${skillPath}`);
318
+ }
319
+ const skillMarkdownPath = path.join(sourceDir, 'SKILL.md');
320
+ if (!(await fs.pathExists(skillMarkdownPath))) {
321
+ throw new Error(`Missing SKILL.md in imported skill directory: ${skillPath}`);
322
+ }
323
+ }
324
+ function toError({ prefix, error }) {
325
+ if (error instanceof Error) {
326
+ return new Error(`${prefix}: ${error.message}`);
327
+ }
328
+ return new Error(`${prefix}: ${String(error)}`);
329
+ }
package/dest/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
package/dest/main.js ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { z } from 'zod';
7
+ import { runInstallCommand } from './commands/install.js';
8
+ import { addSkillsCommand } from './commands/skills/index.js';
9
+ import { nonEmptyOptionStringSchema, parseOptionValue, parseOptionsObject, } from './lib/command-options.js';
10
+ import { createAgentsContext, resolveRequestedOutputRoot, } from './lib/context.js';
11
+ const rootOptionsSchema = z.object({
12
+ test: z.boolean().optional().default(false),
13
+ input: nonEmptyOptionStringSchema.optional(),
14
+ output: nonEmptyOptionStringSchema.optional(),
15
+ });
16
+ /**
17
+ * Reads the CLI version from the package manifest at the repository root.
18
+ */
19
+ async function readCliVersion() {
20
+ const currentFilePath = fileURLToPath(import.meta.url);
21
+ const packageJsonPath = path.resolve(currentFilePath, '..', '..', 'package.json');
22
+ const rawPackageJson = await fs.readFile(packageJsonPath, 'utf8');
23
+ const parsedPackageJson = JSON.parse(rawPackageJson);
24
+ const packageJsonSchema = z.object({
25
+ version: z.string().min(1),
26
+ });
27
+ return packageJsonSchema.parse(parsedPackageJson).version;
28
+ }
29
+ /**
30
+ * Parses the top-level CLI options into a validated shape.
31
+ */
32
+ function getRootOptions(program) {
33
+ return parseOptionsObject({
34
+ schema: rootOptionsSchema,
35
+ options: program.opts(),
36
+ optionsLabel: 'root options',
37
+ });
38
+ }
39
+ /**
40
+ * Returns the active context after applying root-level input and output
41
+ * overrides.
42
+ */
43
+ function resolveActiveContext(program) {
44
+ const rootOptions = getRootOptions(program);
45
+ const requestedOutputRoot = resolveRequestedOutputRoot({
46
+ test: rootOptions.test,
47
+ ...(rootOptions.output ? { outputRoot: rootOptions.output } : {}),
48
+ });
49
+ const context = createAgentsContext({
50
+ ...(rootOptions.input ? { inputRoot: rootOptions.input } : {}),
51
+ ...(requestedOutputRoot ? { outputRoot: requestedOutputRoot } : {}),
52
+ });
53
+ return context;
54
+ }
55
+ /**
56
+ * Configures and runs the agents CLI entrypoint.
57
+ */
58
+ async function main() {
59
+ const executableName = 'dryai';
60
+ const program = new Command();
61
+ const cliVersion = await readCliVersion();
62
+ program
63
+ .name(executableName)
64
+ .usage('[options] <command> [args]')
65
+ .helpOption('-h, --help', 'Display this message')
66
+ .version(cliVersion, '-v, --version', 'Display the current version')
67
+ .option('--test', 'Shortcut for writing generated output into ./output-test unless --output is also provided')
68
+ .option('--input <path>', 'Read input configs from a different root instead of ~/.config/agents', parseOptionValue({
69
+ schema: nonEmptyOptionStringSchema,
70
+ optionLabel: '--input',
71
+ }))
72
+ .option('--output <path>', 'Write generated output under a different root instead of the default home directory', parseOptionValue({
73
+ schema: nonEmptyOptionStringSchema,
74
+ optionLabel: '--output',
75
+ }))
76
+ .helpCommand(false)
77
+ .action(() => {
78
+ program.outputHelp();
79
+ });
80
+ program
81
+ .command('install')
82
+ .description('Install generated output into Copilot and Cursor targets')
83
+ .usage('install')
84
+ .action(async () => {
85
+ const activeContext = resolveActiveContext(program);
86
+ await runInstallCommand(activeContext);
87
+ if (requestedOutputRootWasUsed(program)) {
88
+ console.log(`Generated output written to ${activeContext.outputRoot}`);
89
+ }
90
+ });
91
+ addSkillsCommand({
92
+ parent: program,
93
+ commandName: `${executableName} skills`,
94
+ resolveContext: () => resolveActiveContext(program),
95
+ });
96
+ await program.parseAsync(process.argv);
97
+ }
98
+ /**
99
+ * Returns whether the current invocation requested a non-default output root.
100
+ */
101
+ function requestedOutputRootWasUsed(program) {
102
+ const rootOptions = getRootOptions(program);
103
+ return (resolveRequestedOutputRoot({
104
+ test: rootOptions.test,
105
+ ...(rootOptions.output ? { outputRoot: rootOptions.output } : {}),
106
+ }) !== undefined);
107
+ }
108
+ try {
109
+ await main();
110
+ }
111
+ catch (error) {
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ console.log(message);
114
+ process.exitCode = 1;
115
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "dryai",
3
+ "version": "0.2.1",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "files": [
9
+ "dest/**/*.js",
10
+ "dest/**/*.d.ts"
11
+ ],
12
+ "scripts": {
13
+ "setup:editor": "effect-language-service patch",
14
+ "build": "tsc -p tsconfig.build.json",
15
+ "dev": "tsc -p tsconfig.build.json --watch",
16
+ "dev:dryai": "node ./dest/main.js",
17
+ "tsc": "tsc -b",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest"
20
+ },
21
+ "bin": {
22
+ "dryai": "dest/main.js"
23
+ },
24
+ "dependencies": {
25
+ "@effect/cli": "^0.75.0",
26
+ "chalk": "^5.6.2",
27
+ "commander": "^14.0.3",
28
+ "dedent": "^1.7.2",
29
+ "effect": "^3.21.0",
30
+ "fs-extra": "^11.3.4",
31
+ "glob": "^13.0.6",
32
+ "gray-matter": "^4.0.3",
33
+ "simple-git": "^3.35.2",
34
+ "zod": "^3.24.1"
35
+ },
36
+ "devDependencies": {
37
+ "@effect/language-service": "^0.85.0",
38
+ "@effect/vitest": "^0.29.0",
39
+ "@types/fs-extra": "^11.0.4",
40
+ "@types/node": "^24.0.0",
41
+ "typescript": "^5.8.3",
42
+ "vitest": "^4.1.4"
43
+ },
44
+ "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
45
+ }