@soleri/cli 9.7.2 → 9.8.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/dist/commands/add-domain.js +1 -0
- package/dist/commands/add-domain.js.map +1 -1
- package/dist/commands/add-pack.js +7 -147
- package/dist/commands/add-pack.js.map +1 -1
- package/dist/commands/agent.js +130 -0
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/create.js +78 -2
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.js +2 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/extend.js +17 -0
- package/dist/commands/extend.js.map +1 -1
- package/dist/commands/install-knowledge.js +1 -0
- package/dist/commands/install-knowledge.js.map +1 -1
- package/dist/commands/test.js +140 -1
- package/dist/commands/test.js.map +1 -1
- package/dist/hook-packs/flock-guard/manifest.json +2 -1
- package/dist/hook-packs/marketing-research/manifest.json +2 -1
- package/dist/hook-packs/registry.d.ts +2 -0
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +2 -0
- package/dist/prompts/create-wizard.d.ts +16 -2
- package/dist/prompts/create-wizard.js +84 -11
- package/dist/prompts/create-wizard.js.map +1 -1
- package/dist/utils/checks.d.ts +8 -5
- package/dist/utils/checks.js +105 -10
- package/dist/utils/checks.js.map +1 -1
- package/dist/utils/format-paths.d.ts +14 -0
- package/dist/utils/format-paths.js +27 -0
- package/dist/utils/format-paths.js.map +1 -0
- package/dist/utils/git.d.ts +29 -0
- package/dist/utils/git.js +88 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.js +4 -0
- package/dist/utils/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/create-wizard-git.test.ts +208 -0
- package/src/__tests__/git-utils.test.ts +268 -0
- package/src/__tests__/scaffold-git-e2e.test.ts +105 -0
- package/src/commands/add-domain.ts +1 -0
- package/src/commands/add-pack.ts +10 -163
- package/src/commands/agent.ts +161 -0
- package/src/commands/create.ts +89 -3
- package/src/commands/doctor.ts +1 -0
- package/src/commands/extend.ts +20 -1
- package/src/commands/install-knowledge.ts +1 -0
- package/src/commands/test.ts +141 -2
- package/src/hook-packs/flock-guard/manifest.json +2 -1
- package/src/hook-packs/marketing-research/manifest.json +2 -1
- package/src/hook-packs/registry.ts +2 -0
- package/src/prompts/create-wizard.ts +109 -14
- package/src/utils/checks.ts +122 -13
- package/src/utils/format-paths.ts +41 -0
- package/src/utils/git.ts +118 -0
- package/src/utils/logger.ts +5 -0
|
@@ -8,6 +8,23 @@
|
|
|
8
8
|
import * as p from '@clack/prompts';
|
|
9
9
|
import type { AgentConfigInput } from '@soleri/forge/lib';
|
|
10
10
|
import { ITALIAN_CRAFTSPERSON } from '@soleri/core/personas';
|
|
11
|
+
import { isGhInstalled } from '../utils/git.js';
|
|
12
|
+
|
|
13
|
+
/** Git configuration collected from the wizard. */
|
|
14
|
+
export interface WizardGitConfig {
|
|
15
|
+
init: boolean;
|
|
16
|
+
remote?: {
|
|
17
|
+
type: 'gh' | 'manual';
|
|
18
|
+
url?: string;
|
|
19
|
+
visibility?: 'public' | 'private';
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Full result from the create wizard. */
|
|
24
|
+
export interface CreateWizardResult {
|
|
25
|
+
config: AgentConfigInput;
|
|
26
|
+
git: WizardGitConfig;
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
/** Slugify a display name into a kebab-case ID. */
|
|
13
30
|
function slugify(name: string): string {
|
|
@@ -19,9 +36,9 @@ function slugify(name: string): string {
|
|
|
19
36
|
|
|
20
37
|
/**
|
|
21
38
|
* Run the simplified create wizard.
|
|
22
|
-
* Returns
|
|
39
|
+
* Returns a CreateWizardResult or null if cancelled.
|
|
23
40
|
*/
|
|
24
|
-
export async function runCreateWizard(initialName?: string): Promise<
|
|
41
|
+
export async function runCreateWizard(initialName?: string): Promise<CreateWizardResult | null> {
|
|
25
42
|
p.intro('Create a new Soleri agent');
|
|
26
43
|
|
|
27
44
|
// ─── Step 1: Name ───────────────────────────────────────────
|
|
@@ -119,17 +136,95 @@ export async function runCreateWizard(initialName?: string): Promise<AgentConfig
|
|
|
119
136
|
|
|
120
137
|
if (p.isCancel(confirm) || !confirm) return null;
|
|
121
138
|
|
|
139
|
+
// ─── Step 3: Git setup ──────────────────────────────────────
|
|
140
|
+
const gitInit = await p.confirm({
|
|
141
|
+
message: 'Initialize as a git repository?',
|
|
142
|
+
initialValue: true,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (p.isCancel(gitInit)) return null;
|
|
146
|
+
|
|
147
|
+
const git: WizardGitConfig = { init: gitInit as boolean };
|
|
148
|
+
|
|
149
|
+
if (git.init) {
|
|
150
|
+
const pushRemote = await p.confirm({
|
|
151
|
+
message: 'Push to a remote repository?',
|
|
152
|
+
initialValue: false,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (p.isCancel(pushRemote)) return null;
|
|
156
|
+
|
|
157
|
+
if (pushRemote) {
|
|
158
|
+
const ghAvailable = await isGhInstalled();
|
|
159
|
+
|
|
160
|
+
let remoteType: 'gh' | 'manual';
|
|
161
|
+
|
|
162
|
+
if (ghAvailable) {
|
|
163
|
+
const remoteChoice = await p.select({
|
|
164
|
+
message: 'How would you like to set up the remote?',
|
|
165
|
+
options: [
|
|
166
|
+
{ value: 'gh' as const, label: 'Create a new GitHub repository' },
|
|
167
|
+
{ value: 'manual' as const, label: 'Add an existing remote URL' },
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (p.isCancel(remoteChoice)) return null;
|
|
172
|
+
remoteType = remoteChoice as 'gh' | 'manual';
|
|
173
|
+
} else {
|
|
174
|
+
remoteType = 'manual';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (remoteType === 'gh') {
|
|
178
|
+
const visibility = await p.select({
|
|
179
|
+
message: 'Repository visibility?',
|
|
180
|
+
options: [
|
|
181
|
+
{ value: 'private' as const, label: 'Private' },
|
|
182
|
+
{ value: 'public' as const, label: 'Public' },
|
|
183
|
+
],
|
|
184
|
+
initialValue: 'private' as const,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (p.isCancel(visibility)) return null;
|
|
188
|
+
|
|
189
|
+
git.remote = {
|
|
190
|
+
type: 'gh',
|
|
191
|
+
visibility: visibility as 'public' | 'private',
|
|
192
|
+
};
|
|
193
|
+
} else {
|
|
194
|
+
const remoteUrl = await p.text({
|
|
195
|
+
message: 'Remote repository URL:',
|
|
196
|
+
placeholder: 'https://github.com/user/repo.git',
|
|
197
|
+
validate: (v) => {
|
|
198
|
+
if (!v || v.trim().length === 0) return 'URL is required';
|
|
199
|
+
if (!v.startsWith('https://') && !v.startsWith('git@'))
|
|
200
|
+
return 'URL must start with https:// or git@';
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (p.isCancel(remoteUrl)) return null;
|
|
205
|
+
|
|
206
|
+
git.remote = {
|
|
207
|
+
type: 'manual',
|
|
208
|
+
url: (remoteUrl as string).trim(),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
122
214
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
215
|
+
config: {
|
|
216
|
+
id,
|
|
217
|
+
name: name.trim(),
|
|
218
|
+
role: 'Your universal second brain — learns, remembers, improves',
|
|
219
|
+
description:
|
|
220
|
+
'A universal assistant that learns from your projects, captures knowledge, and gets smarter with every session.',
|
|
221
|
+
domains: [],
|
|
222
|
+
principles: [],
|
|
223
|
+
skills: [],
|
|
224
|
+
tone: 'mentor',
|
|
225
|
+
greeting,
|
|
226
|
+
persona,
|
|
227
|
+
} as AgentConfigInput,
|
|
228
|
+
git,
|
|
229
|
+
};
|
|
135
230
|
}
|
package/src/utils/checks.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Health check utilities for the doctor command.
|
|
3
3
|
*/
|
|
4
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
-
import { detectAgent } from './agent-context.js';
|
|
8
|
+
import { detectAgent, type AgentFormat } from './agent-context.js';
|
|
9
9
|
import { getInstalledPacks } from '../hook-packs/registry.js';
|
|
10
10
|
|
|
11
|
-
interface CheckResult {
|
|
12
|
-
status: 'pass' | 'fail' | 'warn';
|
|
11
|
+
export interface CheckResult {
|
|
12
|
+
status: 'pass' | 'fail' | 'warn' | 'skip';
|
|
13
13
|
label: string;
|
|
14
14
|
detail?: string;
|
|
15
15
|
}
|
|
@@ -53,10 +53,19 @@ export function checkAgentProject(dir?: string): CheckResult {
|
|
|
53
53
|
if (!ctx) {
|
|
54
54
|
return { status: 'warn', label: 'Agent project', detail: 'not detected in current directory' };
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
const formatLabel = ctx.format === 'filetree' ? 'file-tree' : 'typescript';
|
|
57
|
+
return {
|
|
58
|
+
status: 'pass',
|
|
59
|
+
label: 'Agent project',
|
|
60
|
+
detail: `${ctx.agentId} (${ctx.packageName}, ${formatLabel})`,
|
|
61
|
+
};
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
export function checkAgentBuild(dir?: string): CheckResult {
|
|
64
|
+
export function checkAgentBuild(dir?: string, format?: AgentFormat): CheckResult {
|
|
65
|
+
if (format === 'filetree') {
|
|
66
|
+
return { status: 'skip', label: 'Agent build', detail: 'not applicable for file-tree agents' };
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
const ctx = detectAgent(dir);
|
|
61
70
|
if (!ctx) return { status: 'warn', label: 'Agent build', detail: 'no agent detected' };
|
|
62
71
|
|
|
@@ -73,7 +82,15 @@ export function checkAgentBuild(dir?: string): CheckResult {
|
|
|
73
82
|
return { status: 'pass', label: 'Agent build', detail: 'dist/index.js exists' };
|
|
74
83
|
}
|
|
75
84
|
|
|
76
|
-
export function checkNodeModules(dir?: string): CheckResult {
|
|
85
|
+
export function checkNodeModules(dir?: string, format?: AgentFormat): CheckResult {
|
|
86
|
+
if (format === 'filetree') {
|
|
87
|
+
return {
|
|
88
|
+
status: 'skip',
|
|
89
|
+
label: 'Dependencies',
|
|
90
|
+
detail: 'not applicable for file-tree agents',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
const ctx = detectAgent(dir);
|
|
78
95
|
if (!ctx) return { status: 'warn', label: 'Dependencies', detail: 'no agent detected' };
|
|
79
96
|
|
|
@@ -87,6 +104,80 @@ export function checkNodeModules(dir?: string): CheckResult {
|
|
|
87
104
|
return { status: 'pass', label: 'Dependencies', detail: 'node_modules/ exists' };
|
|
88
105
|
}
|
|
89
106
|
|
|
107
|
+
export function checkAgentYaml(agentPath: string): CheckResult {
|
|
108
|
+
const yamlPath = join(agentPath, 'agent.yaml');
|
|
109
|
+
if (!existsSync(yamlPath)) {
|
|
110
|
+
return { status: 'fail', label: 'agent.yaml', detail: 'not found' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const content = readFileSync(yamlPath, 'utf-8');
|
|
115
|
+
// Light validation: check for required fields without pulling in a YAML parser
|
|
116
|
+
// (detectAgent already parsed it, but we verify the raw content for diagnostics)
|
|
117
|
+
const hasId = /^id\s*:/m.test(content);
|
|
118
|
+
const hasName = /^name\s*:/m.test(content);
|
|
119
|
+
|
|
120
|
+
if (!hasId && !hasName) {
|
|
121
|
+
return {
|
|
122
|
+
status: 'fail',
|
|
123
|
+
label: 'agent.yaml',
|
|
124
|
+
detail: 'missing required fields: id, name',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (!hasId) {
|
|
128
|
+
return { status: 'fail', label: 'agent.yaml', detail: 'missing required field: id' };
|
|
129
|
+
}
|
|
130
|
+
if (!hasName) {
|
|
131
|
+
return { status: 'fail', label: 'agent.yaml', detail: 'missing required field: name' };
|
|
132
|
+
}
|
|
133
|
+
return { status: 'pass', label: 'agent.yaml', detail: 'valid (id, name present)' };
|
|
134
|
+
} catch {
|
|
135
|
+
return { status: 'fail', label: 'agent.yaml', detail: 'failed to read file' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function checkInstructionsDir(agentPath: string): CheckResult {
|
|
140
|
+
const instrDir = join(agentPath, 'instructions');
|
|
141
|
+
if (!existsSync(instrDir)) {
|
|
142
|
+
return {
|
|
143
|
+
status: 'fail',
|
|
144
|
+
label: 'Instructions',
|
|
145
|
+
detail: 'instructions/ directory not found',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const files = readdirSync(instrDir).filter((f) => f.endsWith('.md'));
|
|
151
|
+
if (files.length === 0) {
|
|
152
|
+
return {
|
|
153
|
+
status: 'warn',
|
|
154
|
+
label: 'Instructions',
|
|
155
|
+
detail: 'instructions/ exists but contains no .md files',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
status: 'pass',
|
|
160
|
+
label: 'Instructions',
|
|
161
|
+
detail: `${files.length} instruction file${files.length === 1 ? '' : 's'}`,
|
|
162
|
+
};
|
|
163
|
+
} catch {
|
|
164
|
+
return { status: 'fail', label: 'Instructions', detail: 'failed to read instructions/' };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function checkEngineReachable(): CheckResult {
|
|
169
|
+
try {
|
|
170
|
+
require.resolve('@soleri/core/package.json');
|
|
171
|
+
return { status: 'pass', label: 'Engine', detail: '@soleri/core reachable' };
|
|
172
|
+
} catch {
|
|
173
|
+
return {
|
|
174
|
+
status: 'fail',
|
|
175
|
+
label: 'Engine',
|
|
176
|
+
detail: '@soleri/core not found — engine is required for file-tree agents',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
90
181
|
function checkMcpRegistration(dir?: string): CheckResult {
|
|
91
182
|
const ctx = detectAgent(dir);
|
|
92
183
|
if (!ctx) return { status: 'warn', label: 'MCP registration', detail: 'no agent detected' };
|
|
@@ -157,15 +248,33 @@ function checkHookPacks(): CheckResult {
|
|
|
157
248
|
}
|
|
158
249
|
|
|
159
250
|
export function runAllChecks(dir?: string): CheckResult[] {
|
|
160
|
-
|
|
251
|
+
const ctx = detectAgent(dir);
|
|
252
|
+
const format = ctx?.format;
|
|
253
|
+
|
|
254
|
+
// Common checks for all agent formats
|
|
255
|
+
const results: CheckResult[] = [
|
|
161
256
|
checkNodeVersion(),
|
|
162
257
|
checkNpm(),
|
|
163
258
|
checkTsx(),
|
|
164
259
|
checkAgentProject(dir),
|
|
165
|
-
checkNodeModules(dir),
|
|
166
|
-
checkAgentBuild(dir),
|
|
167
|
-
checkMcpRegistration(dir),
|
|
168
|
-
checkHookPacks(),
|
|
169
|
-
checkCognee(),
|
|
170
260
|
];
|
|
261
|
+
|
|
262
|
+
if (format === 'filetree') {
|
|
263
|
+
// File-tree agent checks
|
|
264
|
+
results.push(
|
|
265
|
+
checkAgentYaml(ctx!.agentPath),
|
|
266
|
+
checkInstructionsDir(ctx!.agentPath),
|
|
267
|
+
checkEngineReachable(),
|
|
268
|
+
checkNodeModules(dir, format),
|
|
269
|
+
checkAgentBuild(dir, format),
|
|
270
|
+
);
|
|
271
|
+
} else {
|
|
272
|
+
// TypeScript agent checks (or no agent detected)
|
|
273
|
+
results.push(checkNodeModules(dir, format), checkAgentBuild(dir, format));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Shared checks
|
|
277
|
+
results.push(checkMcpRegistration(dir), checkHookPacks(), checkCognee());
|
|
278
|
+
|
|
279
|
+
return results;
|
|
171
280
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format-aware path resolution for filetree vs typescript agents.
|
|
3
|
+
*/
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export interface FormatPaths {
|
|
7
|
+
knowledgeDir: string;
|
|
8
|
+
extensionsDir: string;
|
|
9
|
+
facadesDir: string;
|
|
10
|
+
agentConfigFile: string;
|
|
11
|
+
entryPoint: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getFormatPaths(ctx: {
|
|
15
|
+
format: 'filetree' | 'typescript';
|
|
16
|
+
agentPath: string;
|
|
17
|
+
}): FormatPaths {
|
|
18
|
+
const { format, agentPath } = ctx;
|
|
19
|
+
|
|
20
|
+
if (format === 'filetree') {
|
|
21
|
+
return {
|
|
22
|
+
knowledgeDir: join(agentPath, 'knowledge'),
|
|
23
|
+
extensionsDir: join(agentPath, 'extensions'),
|
|
24
|
+
facadesDir: '',
|
|
25
|
+
agentConfigFile: join(agentPath, 'agent.yaml'),
|
|
26
|
+
entryPoint: join(agentPath, 'agent.yaml'),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
knowledgeDir: join(agentPath, 'src', 'intelligence', 'data'),
|
|
32
|
+
extensionsDir: join(agentPath, 'src', 'extensions'),
|
|
33
|
+
facadesDir: join(agentPath, 'src', 'facades'),
|
|
34
|
+
agentConfigFile: join(agentPath, 'package.json'),
|
|
35
|
+
entryPoint: join(agentPath, 'src', 'index.ts'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isFileTree(ctx: { format: string }): boolean {
|
|
40
|
+
return ctx.format === 'filetree';
|
|
41
|
+
}
|
package/src/utils/git.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git utility functions for the Soleri CLI scaffold flow.
|
|
3
|
+
*
|
|
4
|
+
* Uses child_process.execFile (not exec) for security — no shell interpolation.
|
|
5
|
+
* Never throws — all functions return { ok, error? } for graceful handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
export interface GitResult {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Default timeout for local git operations (30s). */
|
|
16
|
+
const LOCAL_TIMEOUT = 30_000;
|
|
17
|
+
|
|
18
|
+
/** Timeout for network operations — push, gh create (60s). */
|
|
19
|
+
const NETWORK_TIMEOUT = 60_000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Run a command via execFile and return stdout on success, or an error string on failure.
|
|
23
|
+
*/
|
|
24
|
+
function run(
|
|
25
|
+
cmd: string,
|
|
26
|
+
args: string[],
|
|
27
|
+
options: { cwd?: string; timeout: number },
|
|
28
|
+
): Promise<{ stdout: string; error?: string }> {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
try {
|
|
31
|
+
execFile(
|
|
32
|
+
cmd,
|
|
33
|
+
args,
|
|
34
|
+
{
|
|
35
|
+
cwd: options.cwd,
|
|
36
|
+
signal: AbortSignal.timeout(options.timeout),
|
|
37
|
+
},
|
|
38
|
+
(error, stdout, stderr) => {
|
|
39
|
+
if (error) {
|
|
40
|
+
resolve({ stdout: '', error: stderr?.trim() || error.message });
|
|
41
|
+
} else {
|
|
42
|
+
resolve({ stdout: stdout ?? '' });
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
} catch (err: unknown) {
|
|
47
|
+
// execFile itself can throw (e.g. ENOENT)
|
|
48
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
49
|
+
resolve({ stdout: '', error: msg });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Check if the `git` binary is available on PATH. */
|
|
55
|
+
export async function isGitInstalled(): Promise<boolean> {
|
|
56
|
+
const { error } = await run('which', ['git'], { timeout: LOCAL_TIMEOUT });
|
|
57
|
+
return !error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Check if the `gh` (GitHub CLI) binary is available on PATH. */
|
|
61
|
+
export async function isGhInstalled(): Promise<boolean> {
|
|
62
|
+
const { error } = await run('which', ['gh'], { timeout: LOCAL_TIMEOUT });
|
|
63
|
+
return !error;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Run `git init` in the given directory. */
|
|
67
|
+
export async function gitInit(dir: string): Promise<GitResult> {
|
|
68
|
+
const { error } = await run('git', ['init'], { cwd: dir, timeout: LOCAL_TIMEOUT });
|
|
69
|
+
return error ? { ok: false, error } : { ok: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Stage all files and create an initial commit. */
|
|
73
|
+
export async function gitInitialCommit(dir: string, message: string): Promise<GitResult> {
|
|
74
|
+
const add = await run('git', ['add', '.'], { cwd: dir, timeout: LOCAL_TIMEOUT });
|
|
75
|
+
if (add.error) return { ok: false, error: add.error };
|
|
76
|
+
|
|
77
|
+
const commit = await run('git', ['commit', '-m', message], { cwd: dir, timeout: LOCAL_TIMEOUT });
|
|
78
|
+
if (commit.error) return { ok: false, error: commit.error };
|
|
79
|
+
|
|
80
|
+
return { ok: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Add a remote origin URL. */
|
|
84
|
+
export async function gitAddRemote(dir: string, url: string): Promise<GitResult> {
|
|
85
|
+
const { error } = await run('git', ['remote', 'add', 'origin', url], {
|
|
86
|
+
cwd: dir,
|
|
87
|
+
timeout: LOCAL_TIMEOUT,
|
|
88
|
+
});
|
|
89
|
+
return error ? { ok: false, error } : { ok: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Push to origin main with the -u flag. */
|
|
93
|
+
export async function gitPush(dir: string): Promise<GitResult> {
|
|
94
|
+
const { error } = await run('git', ['push', '-u', 'origin', 'main'], {
|
|
95
|
+
cwd: dir,
|
|
96
|
+
timeout: NETWORK_TIMEOUT,
|
|
97
|
+
});
|
|
98
|
+
return error ? { ok: false, error } : { ok: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Create a GitHub repo using the `gh` CLI. */
|
|
102
|
+
export async function ghCreateRepo(
|
|
103
|
+
name: string,
|
|
104
|
+
options: { visibility: 'public' | 'private'; dir: string },
|
|
105
|
+
): Promise<GitResult & { url?: string }> {
|
|
106
|
+
const visFlag = options.visibility === 'public' ? '--public' : '--private';
|
|
107
|
+
const { stdout, error } = await run(
|
|
108
|
+
'gh',
|
|
109
|
+
['repo', 'create', name, visFlag, `--source=${options.dir}`, '--remote=origin', '--push'],
|
|
110
|
+
{ cwd: options.dir, timeout: NETWORK_TIMEOUT },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (error) return { ok: false, error };
|
|
114
|
+
|
|
115
|
+
// gh repo create prints the repo URL on stdout
|
|
116
|
+
const url = stdout.trim() || undefined;
|
|
117
|
+
return { ok: true, url };
|
|
118
|
+
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -26,6 +26,11 @@ export function warn(label: string, detail?: string): void {
|
|
|
26
26
|
console.log(` ${YELLOW}!${RESET} ${label}${suffix}`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function skip(label: string, detail?: string): void {
|
|
30
|
+
const suffix = detail ? ` ${DIM}${detail}${RESET}` : '';
|
|
31
|
+
console.log(` ${DIM}–${RESET} ${label}${suffix}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export function info(message: string): void {
|
|
30
35
|
console.log(` ${CYAN}ℹ${RESET} ${message}`);
|
|
31
36
|
}
|