orizu 0.3.7 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -6
- package/dist/banner.js +20 -0
- package/dist/csv.js +117 -9
- package/dist/help.js +114 -15
- package/dist/index.js +828 -67
- package/dist/setup-prompt.js +22 -0
- package/dist/skill-installer.js +243 -17
- package/dist/task-assignment-manifest.js +59 -0
- package/dist/workspace.js +76 -0
- package/package.json +5 -1
- package/src/banner.ts +21 -0
- package/src/csv.ts +140 -10
- package/src/help.ts +114 -15
- package/src/index.ts +933 -70
- package/src/setup-prompt.ts +27 -0
- package/src/skill-installer.ts +343 -17
- package/src/task-assignment-manifest.ts +96 -0
- package/src/workspace.ts +112 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/__init__.py +2 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +57 -3
- package/vendor/orizu-gepa-python/src/orizu_gepa/client.py +37 -1
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +233 -17
- package/vendor/orizu-gepa-python/src/orizu_gepa/reflection.py +98 -17
- package/vendor/skills/orizu-cli/SKILL.md +13 -6
- package/vendor/skills/orizu-cli/references/cli-reference.md +34 -5
- package/vendor/skills/orizu-cli/references/optimization-reports.md +2 -2
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +1 -0
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +17 -5
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function renderAgentSetupPrompt(options) {
|
|
2
|
+
const workspaceLine = options?.workspacePath
|
|
3
|
+
? `- Local Orizu artifacts belong under ${options.workspacePath} (gitignored). Keep secrets out of the repo; credentials live in ~/.config/orizu.`
|
|
4
|
+
: '- If local Orizu artifacts are needed, suggest creating a gitignored .orizu/ workspace (orizu setup can do this).';
|
|
5
|
+
return `Help me embed Orizu (eval-first LLM optimization) into this repository's workflow.
|
|
6
|
+
|
|
7
|
+
Start by learning the Orizu workflow:
|
|
8
|
+
1. Run \`orizu skills path --json\` and read the SKILL.md it points to, plus the references it links.
|
|
9
|
+
2. Run \`orizu --version\` and \`orizu whoami\` to confirm the CLI is installed and authenticated. If either fails, pause and tell me how to fix it (npm i -g orizu, orizu login) before continuing.
|
|
10
|
+
|
|
11
|
+
Then inspect this repository and propose how Orizu should fit it:
|
|
12
|
+
- Identify the LLM application surfaces worth evaluating (prompts, agents, pipelines) and where their inputs/outputs live.
|
|
13
|
+
- Propose Orizu team(s) and project(s) that match how this repo is organized.
|
|
14
|
+
- Propose datasets: existing data that can be uploaded now, or a concrete plan to collect examples if none exists.
|
|
15
|
+
- Propose which prompts to version in Orizu, and which scorers or LLM judges would capture quality for them.
|
|
16
|
+
- Propose runner artifacts where local execution is needed.
|
|
17
|
+
${workspaceLine}
|
|
18
|
+
|
|
19
|
+
Important boundaries:
|
|
20
|
+
- Present your plan and get my approval before creating anything on the Orizu server or changing repo files.
|
|
21
|
+
- Never duplicate Orizu runtime behavior (scoring, optimization, auth) in repo code; always go through the orizu CLI.`;
|
|
22
|
+
}
|
package/dist/skill-installer.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from 'fs';
|
|
2
|
-
import { dirname, isAbsolute, relative, resolve } from 'path';
|
|
1
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync, } from 'fs';
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
5
6
|
export const SKILL_NAME = 'orizu-cli';
|
|
6
7
|
export const AGENTS_START_MARKER = '<!-- orizu-cli:start -->';
|
|
7
8
|
export const AGENTS_END_MARKER = '<!-- orizu-cli:end -->';
|
|
8
9
|
export const SKILL_INSTALL_TARGETS = [
|
|
9
10
|
'agent-user',
|
|
11
|
+
'agents-project',
|
|
10
12
|
'codex-project',
|
|
11
13
|
'claude-user',
|
|
12
14
|
'claude-project',
|
|
13
15
|
'agents-md',
|
|
14
16
|
];
|
|
17
|
+
export const SKILL_META_FILENAME = '.orizu-skill-meta.json';
|
|
18
|
+
export const SKILL_INSTALL_AGENTS = ['claude', 'codex'];
|
|
19
|
+
const PROJECT_LEVEL_TARGETS = [
|
|
20
|
+
'agents-project',
|
|
21
|
+
'codex-project',
|
|
22
|
+
'claude-project',
|
|
23
|
+
];
|
|
15
24
|
function getCliRoot() {
|
|
16
25
|
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
26
|
}
|
|
@@ -25,23 +34,38 @@ export function formatMissingSkillSourceError(paths) {
|
|
|
25
34
|
return `Unable to find bundled ${SKILL_NAME} skill. Reinstall the orizu CLI or run from the Orizu repository.\n`
|
|
26
35
|
+ `Searched:\n- ${paths.join('\n- ')}`;
|
|
27
36
|
}
|
|
28
|
-
export function
|
|
37
|
+
export function resolveSkillSource() {
|
|
29
38
|
const override = process.env.ORIZU_SKILL_SOURCE_DIR;
|
|
30
39
|
if (override) {
|
|
31
40
|
const resolvedOverride = resolve(override);
|
|
32
41
|
if (!isSkillDir(resolvedOverride)) {
|
|
33
42
|
throw new Error(`ORIZU_SKILL_SOURCE_DIR is not an Orizu skill directory: ${resolvedOverride}`);
|
|
34
43
|
}
|
|
35
|
-
return
|
|
44
|
+
return {
|
|
45
|
+
name: SKILL_NAME,
|
|
46
|
+
root: resolvedOverride,
|
|
47
|
+
skillMd: resolve(resolvedOverride, 'SKILL.md'),
|
|
48
|
+
source: 'override',
|
|
49
|
+
};
|
|
36
50
|
}
|
|
37
51
|
const cliRoot = getCliRoot();
|
|
38
52
|
const packagedSkill = resolve(cliRoot, 'vendor', 'skills', SKILL_NAME);
|
|
39
53
|
if (isSkillDir(packagedSkill)) {
|
|
40
|
-
return
|
|
54
|
+
return {
|
|
55
|
+
name: SKILL_NAME,
|
|
56
|
+
root: packagedSkill,
|
|
57
|
+
skillMd: resolve(packagedSkill, 'SKILL.md'),
|
|
58
|
+
source: 'packaged',
|
|
59
|
+
};
|
|
41
60
|
}
|
|
42
61
|
const repoSkill = resolve(cliRoot, '..', '..', 'skills', SKILL_NAME);
|
|
43
62
|
if (isSkillDir(repoSkill)) {
|
|
44
|
-
return
|
|
63
|
+
return {
|
|
64
|
+
name: SKILL_NAME,
|
|
65
|
+
root: repoSkill,
|
|
66
|
+
skillMd: resolve(repoSkill, 'SKILL.md'),
|
|
67
|
+
source: 'repo-fallback',
|
|
68
|
+
};
|
|
45
69
|
}
|
|
46
70
|
const searchedPaths = [
|
|
47
71
|
`packaged skill: ${packagedSkill}`,
|
|
@@ -49,6 +73,38 @@ export function resolveSkillSourceDir() {
|
|
|
49
73
|
];
|
|
50
74
|
throw new Error(formatMissingSkillSourceError(searchedPaths));
|
|
51
75
|
}
|
|
76
|
+
export function resolveSkillSourceDir() {
|
|
77
|
+
return resolveSkillSource().root;
|
|
78
|
+
}
|
|
79
|
+
function listSkillFiles(root, dir, files) {
|
|
80
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
81
|
+
const entryPath = join(dir, entry.name);
|
|
82
|
+
if (!shouldCopySkillPath(root, entryPath)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
listSkillFiles(root, entryPath, files);
|
|
87
|
+
}
|
|
88
|
+
else if (entry.isFile()) {
|
|
89
|
+
files.push(entryPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function computeSkillContentHash(root) {
|
|
94
|
+
const files = [];
|
|
95
|
+
listSkillFiles(root, root, files);
|
|
96
|
+
const hash = createHash('sha256');
|
|
97
|
+
const sorted = files
|
|
98
|
+
.map(file => relative(root, file).replace(/\\/g, '/'))
|
|
99
|
+
.sort();
|
|
100
|
+
for (const rel of sorted) {
|
|
101
|
+
hash.update(rel);
|
|
102
|
+
hash.update('\0');
|
|
103
|
+
hash.update(readFileSync(join(root, rel)));
|
|
104
|
+
hash.update('\0');
|
|
105
|
+
}
|
|
106
|
+
return `sha256:${hash.digest('hex')}`;
|
|
107
|
+
}
|
|
52
108
|
function resolveHome(options) {
|
|
53
109
|
return resolve(options?.homeDir || process.env.HOME || homedir());
|
|
54
110
|
}
|
|
@@ -61,6 +117,9 @@ export function getSkillInstallPath(target, options) {
|
|
|
61
117
|
if (target === 'agent-user') {
|
|
62
118
|
return resolve(home, '.agents', 'skills', SKILL_NAME);
|
|
63
119
|
}
|
|
120
|
+
if (target === 'agents-project') {
|
|
121
|
+
return resolve(cwd, '.agents', 'skills', SKILL_NAME);
|
|
122
|
+
}
|
|
64
123
|
if (target === 'codex-project') {
|
|
65
124
|
return resolve(cwd, '.codex', 'skills', SKILL_NAME);
|
|
66
125
|
}
|
|
@@ -72,6 +131,52 @@ export function getSkillInstallPath(target, options) {
|
|
|
72
131
|
}
|
|
73
132
|
return resolve(cwd, 'AGENTS.md');
|
|
74
133
|
}
|
|
134
|
+
export function getTargetForAgent(agent, scope) {
|
|
135
|
+
if (agent === 'claude') {
|
|
136
|
+
return scope === 'global' ? 'claude-user' : 'claude-project';
|
|
137
|
+
}
|
|
138
|
+
return scope === 'global' ? 'agent-user' : 'agents-project';
|
|
139
|
+
}
|
|
140
|
+
export function isSkillInstallAgent(value) {
|
|
141
|
+
return SKILL_INSTALL_AGENTS.includes(value);
|
|
142
|
+
}
|
|
143
|
+
export function isProjectLevelTarget(target) {
|
|
144
|
+
return PROJECT_LEVEL_TARGETS.includes(target);
|
|
145
|
+
}
|
|
146
|
+
const UNSTABLE_SOURCE_PATTERNS = [
|
|
147
|
+
/\/_npx\//,
|
|
148
|
+
/\/\.npm\//,
|
|
149
|
+
/\/npm-cache\//,
|
|
150
|
+
/\/\.bun\/install\/cache\//,
|
|
151
|
+
/\/bunx-/,
|
|
152
|
+
/\/dlx-/,
|
|
153
|
+
/\/\.pnpm-store\//,
|
|
154
|
+
/^\/tmp\//,
|
|
155
|
+
/^\/(private\/)?var\/folders\//,
|
|
156
|
+
/\/Temp\//,
|
|
157
|
+
];
|
|
158
|
+
export function isStableSkillSourcePath(root) {
|
|
159
|
+
const normalized = root.replace(/\\/g, '/');
|
|
160
|
+
return !UNSTABLE_SOURCE_PATTERNS.some(pattern => pattern.test(normalized));
|
|
161
|
+
}
|
|
162
|
+
export function resolveEffectiveInstallMode(target, sourceRoot, mode = 'auto') {
|
|
163
|
+
if (target === 'agents-md') {
|
|
164
|
+
return 'section';
|
|
165
|
+
}
|
|
166
|
+
if (isProjectLevelTarget(target)) {
|
|
167
|
+
if (mode === 'link') {
|
|
168
|
+
throw new Error(`--mode link is not supported for project-level target '${target}': committed project files must not symlink to user-local package paths. Use --mode copy.`);
|
|
169
|
+
}
|
|
170
|
+
return 'copy';
|
|
171
|
+
}
|
|
172
|
+
if (mode === 'link') {
|
|
173
|
+
return 'link';
|
|
174
|
+
}
|
|
175
|
+
if (mode === 'copy') {
|
|
176
|
+
return 'copy';
|
|
177
|
+
}
|
|
178
|
+
return isStableSkillSourcePath(sourceRoot) ? 'link' : 'copy';
|
|
179
|
+
}
|
|
75
180
|
function shouldCopySkillPath(sourceRoot, sourcePath) {
|
|
76
181
|
const rel = relative(sourceRoot, sourcePath).replace(/\\/g, '/');
|
|
77
182
|
if (!rel) {
|
|
@@ -83,7 +188,8 @@ function shouldCopySkillPath(sourceRoot, sourcePath) {
|
|
|
83
188
|
const parts = rel.split('/');
|
|
84
189
|
return !parts.some(part => part === '.DS_Store' ||
|
|
85
190
|
part === '__pycache__' ||
|
|
86
|
-
part === '.pytest_cache'
|
|
191
|
+
part === '.pytest_cache' ||
|
|
192
|
+
part === SKILL_META_FILENAME) && !rel.endsWith('.pyc') && !rel.endsWith('.pyo');
|
|
87
193
|
}
|
|
88
194
|
function copySkillTree(sourceDir, targetDir) {
|
|
89
195
|
rmSync(targetDir, { recursive: true, force: true });
|
|
@@ -117,6 +223,7 @@ function installAgentsMd(sourceDir, targetPath, options) {
|
|
|
117
223
|
target: 'agents-md',
|
|
118
224
|
path: targetPath,
|
|
119
225
|
action: action === 'updated' ? 'would-update' : 'would-create',
|
|
226
|
+
mode: 'section',
|
|
120
227
|
};
|
|
121
228
|
}
|
|
122
229
|
if (hasManagedSection && !options?.overwrite) {
|
|
@@ -125,54 +232,173 @@ function installAgentsMd(sourceDir, targetPath, options) {
|
|
|
125
232
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
126
233
|
if (!exists) {
|
|
127
234
|
writeFileSync(targetPath, section, 'utf8');
|
|
128
|
-
return { target: 'agents-md', path: targetPath, action: 'created' };
|
|
235
|
+
return { target: 'agents-md', path: targetPath, action: 'created', mode: 'section' };
|
|
129
236
|
}
|
|
130
237
|
if (hasManagedSection) {
|
|
131
238
|
const pattern = new RegExp(`${escapeRegExp(AGENTS_START_MARKER)}[\\s\\S]*?${escapeRegExp(AGENTS_END_MARKER)}\\n?`);
|
|
132
239
|
writeFileSync(targetPath, existing.replace(pattern, section), 'utf8');
|
|
133
|
-
return { target: 'agents-md', path: targetPath, action: 'updated' };
|
|
240
|
+
return { target: 'agents-md', path: targetPath, action: 'updated', mode: 'section' };
|
|
134
241
|
}
|
|
135
242
|
const existingPrefix = existing.trim().length === 0 ? '' : existing;
|
|
136
243
|
const separator = existingPrefix.endsWith('\n') ? '\n' : '\n\n';
|
|
137
244
|
writeFileSync(targetPath, `${existingPrefix}${existingPrefix ? separator : ''}${section}`, 'utf8');
|
|
138
|
-
return { target: 'agents-md', path: targetPath, action: 'updated' };
|
|
245
|
+
return { target: 'agents-md', path: targetPath, action: 'updated', mode: 'section' };
|
|
139
246
|
}
|
|
140
247
|
function escapeRegExp(value) {
|
|
141
248
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
142
249
|
}
|
|
250
|
+
function pathEntryExists(path) {
|
|
251
|
+
try {
|
|
252
|
+
lstatSync(path);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
143
259
|
export function targetNeedsOverwrite(target, options) {
|
|
144
260
|
const targetPath = getSkillInstallPath(target, options);
|
|
261
|
+
if (target !== 'agents-md') {
|
|
262
|
+
return pathEntryExists(targetPath);
|
|
263
|
+
}
|
|
145
264
|
if (!existsSync(targetPath)) {
|
|
146
265
|
return false;
|
|
147
266
|
}
|
|
148
|
-
if (target !== 'agents-md') {
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
267
|
const existing = readFileSync(targetPath, 'utf8');
|
|
152
268
|
return existing.includes(AGENTS_START_MARKER) && existing.includes(AGENTS_END_MARKER);
|
|
153
269
|
}
|
|
270
|
+
function writeSkillMeta(targetDir, source, options) {
|
|
271
|
+
const meta = {
|
|
272
|
+
name: source.name,
|
|
273
|
+
skillHash: computeSkillContentHash(source.root),
|
|
274
|
+
cliVersion: options?.cliVersion || null,
|
|
275
|
+
source: source.source,
|
|
276
|
+
sourceRoot: source.root,
|
|
277
|
+
mode: 'copy',
|
|
278
|
+
installedAt: new Date().toISOString(),
|
|
279
|
+
};
|
|
280
|
+
writeFileSync(join(targetDir, SKILL_META_FILENAME), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
|
|
281
|
+
}
|
|
282
|
+
export function readSkillMeta(targetDir) {
|
|
283
|
+
const metaPath = join(targetDir, SKILL_META_FILENAME);
|
|
284
|
+
if (!existsSync(metaPath)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
289
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function linkSkillTree(sourceDir, targetDir) {
|
|
296
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
297
|
+
mkdirSync(dirname(targetDir), { recursive: true });
|
|
298
|
+
symlinkSync(sourceDir, targetDir, 'dir');
|
|
299
|
+
}
|
|
154
300
|
export function installSkillTarget(target, options) {
|
|
155
|
-
const
|
|
301
|
+
const source = resolveSkillSource();
|
|
156
302
|
const targetPath = getSkillInstallPath(target, options);
|
|
157
|
-
const exists = existsSync(targetPath);
|
|
158
303
|
if (target === 'agents-md') {
|
|
159
|
-
return installAgentsMd(
|
|
304
|
+
return installAgentsMd(source.root, targetPath, options);
|
|
160
305
|
}
|
|
306
|
+
const exists = pathEntryExists(targetPath);
|
|
307
|
+
const mode = resolveEffectiveInstallMode(target, source.root, options?.mode);
|
|
161
308
|
if (options?.dryRun) {
|
|
162
309
|
return {
|
|
163
310
|
target,
|
|
164
311
|
path: targetPath,
|
|
165
312
|
action: exists ? 'would-update' : 'would-create',
|
|
313
|
+
mode,
|
|
166
314
|
};
|
|
167
315
|
}
|
|
168
316
|
if (exists && !options?.overwrite) {
|
|
169
317
|
throw new Error(`${targetPath} already exists. Pass --yes to replace it.`);
|
|
170
318
|
}
|
|
171
|
-
|
|
319
|
+
if (mode === 'link') {
|
|
320
|
+
linkSkillTree(source.root, targetPath);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
copySkillTree(source.root, targetPath);
|
|
324
|
+
writeSkillMeta(targetPath, source, options);
|
|
325
|
+
}
|
|
172
326
|
return {
|
|
173
327
|
target,
|
|
174
328
|
path: targetPath,
|
|
175
329
|
action: exists ? 'updated' : 'created',
|
|
330
|
+
mode,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function getAgentsMdStatus(targetPath, source, sourceHash) {
|
|
334
|
+
const base = {
|
|
335
|
+
target: 'agents-md',
|
|
336
|
+
path: targetPath,
|
|
337
|
+
mode: 'section',
|
|
338
|
+
linkTarget: null,
|
|
339
|
+
installedHash: null,
|
|
340
|
+
sourceHash,
|
|
341
|
+
meta: null,
|
|
342
|
+
};
|
|
343
|
+
if (!existsSync(targetPath)) {
|
|
344
|
+
return { ...base, state: 'missing' };
|
|
345
|
+
}
|
|
346
|
+
const existing = readFileSync(targetPath, 'utf8');
|
|
347
|
+
const start = existing.indexOf(AGENTS_START_MARKER);
|
|
348
|
+
const end = existing.indexOf(AGENTS_END_MARKER);
|
|
349
|
+
if (start === -1 || end === -1) {
|
|
350
|
+
return { ...base, state: 'unmanaged' };
|
|
351
|
+
}
|
|
352
|
+
const actualSection = existing.slice(start, end + AGENTS_END_MARKER.length);
|
|
353
|
+
const expectedSection = buildAgentsSection(source.root).trimEnd();
|
|
354
|
+
return {
|
|
355
|
+
...base,
|
|
356
|
+
state: actualSection.trimEnd() === expectedSection ? 'current' : 'stale',
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
export function getSkillTargetStatus(target, options) {
|
|
360
|
+
const source = resolveSkillSource();
|
|
361
|
+
const sourceHash = computeSkillContentHash(source.root);
|
|
362
|
+
const targetPath = getSkillInstallPath(target, options);
|
|
363
|
+
if (target === 'agents-md') {
|
|
364
|
+
return getAgentsMdStatus(targetPath, source, sourceHash);
|
|
365
|
+
}
|
|
366
|
+
const base = {
|
|
367
|
+
target,
|
|
368
|
+
path: targetPath,
|
|
369
|
+
linkTarget: null,
|
|
370
|
+
installedHash: null,
|
|
371
|
+
sourceHash,
|
|
372
|
+
meta: null,
|
|
373
|
+
};
|
|
374
|
+
if (!pathEntryExists(targetPath)) {
|
|
375
|
+
return { ...base, state: 'missing', mode: null };
|
|
376
|
+
}
|
|
377
|
+
const stats = lstatSync(targetPath);
|
|
378
|
+
if (stats.isSymbolicLink()) {
|
|
379
|
+
const linkTarget = readlinkSync(targetPath);
|
|
380
|
+
if (!existsSync(targetPath)) {
|
|
381
|
+
return { ...base, state: 'broken-link', mode: 'link', linkTarget };
|
|
382
|
+
}
|
|
383
|
+
const resolvedLink = realpathSync(targetPath);
|
|
384
|
+
const installedHash = computeSkillContentHash(resolvedLink);
|
|
385
|
+
const state = resolvedLink === realpathSync(source.root) || installedHash === sourceHash
|
|
386
|
+
? 'current'
|
|
387
|
+
: 'stale';
|
|
388
|
+
return { ...base, state, mode: 'link', linkTarget, installedHash };
|
|
389
|
+
}
|
|
390
|
+
const installedHash = computeSkillContentHash(targetPath);
|
|
391
|
+
const meta = readSkillMeta(targetPath);
|
|
392
|
+
// A differing directory without our meta file was not installed by Orizu
|
|
393
|
+
// (or was hand-customized): treat it as unmanaged so `skills update` never
|
|
394
|
+
// silently overwrites it. Matching content is current regardless of meta.
|
|
395
|
+
const state = installedHash === sourceHash ? 'current' : meta ? 'stale' : 'unmanaged';
|
|
396
|
+
return {
|
|
397
|
+
...base,
|
|
398
|
+
state,
|
|
399
|
+
mode: 'copy',
|
|
400
|
+
installedHash,
|
|
401
|
+
meta,
|
|
176
402
|
};
|
|
177
403
|
}
|
|
178
404
|
export function isSkillInstallTarget(value) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function parseAssignmentManifestJsonl(content) {
|
|
5
|
+
// Keep these JSONL field rules in sync with the server's explicitAssignments parser.
|
|
6
|
+
const entries = [];
|
|
7
|
+
const lines = content.split(/\r?\n/);
|
|
8
|
+
lines.forEach((line, lineIndex) => {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = JSON.parse(trimmed);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} is not valid JSON`);
|
|
19
|
+
}
|
|
20
|
+
if (!isRecord(parsed)) {
|
|
21
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} must be a JSON object`);
|
|
22
|
+
}
|
|
23
|
+
const rowId = typeof parsed.rowId === 'string' ? parsed.rowId.trim() : '';
|
|
24
|
+
if (!rowId) {
|
|
25
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} must include a non-empty rowId`);
|
|
26
|
+
}
|
|
27
|
+
const hasAssignee = Object.hasOwn(parsed, 'assignee');
|
|
28
|
+
const hasAssignees = Object.hasOwn(parsed, 'assignees');
|
|
29
|
+
if (hasAssignee && hasAssignees) {
|
|
30
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} must provide either assignee or assignees, not both`);
|
|
31
|
+
}
|
|
32
|
+
if (hasAssignee) {
|
|
33
|
+
const assignee = typeof parsed.assignee === 'string' ? parsed.assignee.trim() : '';
|
|
34
|
+
if (!assignee) {
|
|
35
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} assignee must be a non-empty string`);
|
|
36
|
+
}
|
|
37
|
+
entries.push({ rowId, assignee });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (hasAssignees) {
|
|
41
|
+
if (!Array.isArray(parsed.assignees) || parsed.assignees.length === 0) {
|
|
42
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} assignees must be a non-empty string array`);
|
|
43
|
+
}
|
|
44
|
+
parsed.assignees.forEach((assigneeValue, assigneeIndex) => {
|
|
45
|
+
const assignee = typeof assigneeValue === 'string' ? assigneeValue.trim() : '';
|
|
46
|
+
if (!assignee) {
|
|
47
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} assignees[${assigneeIndex}] must be a non-empty string`);
|
|
48
|
+
}
|
|
49
|
+
entries.push({ rowId, assignee });
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Assignment manifest line ${lineIndex + 1} must include assignee or assignees`);
|
|
54
|
+
});
|
|
55
|
+
if (entries.length === 0) {
|
|
56
|
+
throw new Error('Assignment manifest must contain at least one assignment');
|
|
57
|
+
}
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
|
+
export const WORKSPACE_DIR_NAME = '.orizu';
|
|
4
|
+
const WORKSPACE_README = `# Orizu workspace
|
|
5
|
+
|
|
6
|
+
Local Orizu artifacts for this repository. This folder is gitignored by
|
|
7
|
+
default: generated and server-derived content here is not guaranteed to stay
|
|
8
|
+
in sync with the Orizu server, so checking it in would risk drift. Safe,
|
|
9
|
+
user-authored configuration may move to a committed location once a sync model
|
|
10
|
+
exists.
|
|
11
|
+
|
|
12
|
+
- \`workspace.json\`: safe local metadata (no secrets; credentials stay in
|
|
13
|
+
\`~/.config/orizu\`).
|
|
14
|
+
- \`generated/\`: runner artifacts, exports, and other generated files.
|
|
15
|
+
`;
|
|
16
|
+
export function getWorkspaceRoot(cwd) {
|
|
17
|
+
return resolve(cwd || process.cwd(), WORKSPACE_DIR_NAME);
|
|
18
|
+
}
|
|
19
|
+
export function workspaceExists(cwd) {
|
|
20
|
+
return existsSync(join(getWorkspaceRoot(cwd), 'workspace.json'));
|
|
21
|
+
}
|
|
22
|
+
function ensureGitignoreEntry(cwd, dryRun) {
|
|
23
|
+
const gitignorePath = resolve(cwd, '.gitignore');
|
|
24
|
+
const entry = `${WORKSPACE_DIR_NAME}/`;
|
|
25
|
+
if (existsSync(gitignorePath)) {
|
|
26
|
+
const existing = readFileSync(gitignorePath, 'utf8');
|
|
27
|
+
const hasEntry = existing
|
|
28
|
+
.split('\n')
|
|
29
|
+
.some(line => line.trim() === entry || line.trim() === WORKSPACE_DIR_NAME);
|
|
30
|
+
if (hasEntry) {
|
|
31
|
+
return { updated: false, action: null };
|
|
32
|
+
}
|
|
33
|
+
if (!dryRun) {
|
|
34
|
+
const separator = existing.endsWith('\n') || existing.length === 0 ? '' : '\n';
|
|
35
|
+
writeFileSync(gitignorePath, `${existing}${separator}\n# Local Orizu workspace (generated; not synced with the server)\n${entry}\n`, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
return { updated: true, action: `append ${entry} to .gitignore` };
|
|
38
|
+
}
|
|
39
|
+
if (!dryRun) {
|
|
40
|
+
writeFileSync(gitignorePath, `# Local Orizu workspace (generated; not synced with the server)\n${entry}\n`, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
return { updated: true, action: `create .gitignore with ${entry}` };
|
|
43
|
+
}
|
|
44
|
+
export function initOrizuWorkspace(options) {
|
|
45
|
+
const cwd = resolve(options?.cwd || process.cwd());
|
|
46
|
+
const root = getWorkspaceRoot(cwd);
|
|
47
|
+
const dryRun = Boolean(options?.dryRun);
|
|
48
|
+
const exists = workspaceExists(cwd);
|
|
49
|
+
const actions = [];
|
|
50
|
+
if (!exists) {
|
|
51
|
+
actions.push(`create ${WORKSPACE_DIR_NAME}/workspace.json, README.md, and generated/`);
|
|
52
|
+
}
|
|
53
|
+
const gitignore = ensureGitignoreEntry(cwd, dryRun || exists);
|
|
54
|
+
if (!exists && gitignore.action) {
|
|
55
|
+
actions.push(gitignore.action);
|
|
56
|
+
}
|
|
57
|
+
if (dryRun) {
|
|
58
|
+
return {
|
|
59
|
+
root,
|
|
60
|
+
state: exists ? 'exists' : 'would-create',
|
|
61
|
+
gitignoreUpdated: false,
|
|
62
|
+
actions,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (exists) {
|
|
66
|
+
return { root, state: 'exists', gitignoreUpdated: false, actions: [] };
|
|
67
|
+
}
|
|
68
|
+
mkdirSync(join(root, 'generated'), { recursive: true });
|
|
69
|
+
writeFileSync(join(root, 'README.md'), WORKSPACE_README, 'utf8');
|
|
70
|
+
writeFileSync(join(root, 'workspace.json'), `${JSON.stringify({
|
|
71
|
+
version: 1,
|
|
72
|
+
server: options?.baseUrl || null,
|
|
73
|
+
createdByCliVersion: options?.cliVersion || null,
|
|
74
|
+
}, null, 2)}\n`, 'utf8');
|
|
75
|
+
return { root, state: 'created', gitignoreUpdated: gitignore.updated, actions };
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orizu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,5 +27,9 @@
|
|
|
27
27
|
"react": "19.2.6",
|
|
28
28
|
"react-dom": "19.2.6",
|
|
29
29
|
"tailwindcss": "^4.1.10"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^24.0.0",
|
|
33
|
+
"typescript": "5.8.3"
|
|
30
34
|
}
|
|
31
35
|
}
|
package/src/banner.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Faceted rendering of the Orizu origami swan (public/orizu-swan.svg):
|
|
2
|
+
// each polygon is filled with block/triangle glyphs and shrunk slightly so
|
|
3
|
+
// the seams between origami facets stay visible, matching the brand mark.
|
|
4
|
+
// Printed uncolored so it renders in the terminal's default foreground.
|
|
5
|
+
export const ORIZU_BANNER = ` ▄▄
|
|
6
|
+
▄▜▛ ██▙
|
|
7
|
+
▟▀ ◥██▛
|
|
8
|
+
▀▜▄
|
|
9
|
+
▄▟█
|
|
10
|
+
▄███
|
|
11
|
+
▄████◤ ▄▄▄█▛
|
|
12
|
+
▟█████◤ ▄▄█████▛
|
|
13
|
+
▄▀███◤ ▄█████████▛
|
|
14
|
+
◥█▄▀▛▐█▄◥▜███████▛ ███▀
|
|
15
|
+
▜██ ████▄◥▜████▛ ██▀
|
|
16
|
+
█ ███████▄ ▜█▛ █▀
|
|
17
|
+
▐█████████◣ ▀`
|
|
18
|
+
|
|
19
|
+
export function renderBanner(): string {
|
|
20
|
+
return ORIZU_BANNER
|
|
21
|
+
}
|