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.
@@ -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
+ }
@@ -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 resolveSkillSourceDir() {
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 resolvedOverride;
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 packagedSkill;
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 repoSkill;
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') && !rel.endsWith('.pyc') && !rel.endsWith('.pyo');
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 sourceDir = resolveSkillSourceDir();
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(sourceDir, targetPath, options);
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
- copySkillTree(sourceDir, targetPath);
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.7",
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
+ }