@ulysses-ai/create-workspace 0.13.0-beta.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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/bin/create.mjs +79 -0
  4. package/lib/git.mjs +26 -0
  5. package/lib/init.mjs +129 -0
  6. package/lib/payload.mjs +44 -0
  7. package/lib/prompts.mjs +113 -0
  8. package/lib/scaffold.mjs +84 -0
  9. package/lib/upgrade.mjs +42 -0
  10. package/package.json +43 -0
  11. package/template/.claude/agents/aside-researcher.md +48 -0
  12. package/template/.claude/agents/implementer.md +39 -0
  13. package/template/.claude/agents/researcher.md +40 -0
  14. package/template/.claude/agents/reviewer.md +47 -0
  15. package/template/.claude/hooks/_utils.mjs +196 -0
  16. package/template/.claude/hooks/_utils.test.mjs +99 -0
  17. package/template/.claude/hooks/post-compact.mjs +7 -0
  18. package/template/.claude/hooks/pre-compact.mjs +34 -0
  19. package/template/.claude/hooks/repo-write-detection.mjs +107 -0
  20. package/template/.claude/hooks/session-end.mjs +91 -0
  21. package/template/.claude/hooks/session-start.mjs +150 -0
  22. package/template/.claude/hooks/subagent-start.mjs +44 -0
  23. package/template/.claude/hooks/workspace-update-check.mjs +42 -0
  24. package/template/.claude/hooks/worktree-create.mjs +53 -0
  25. package/template/.claude/lib/session-frontmatter.mjs +265 -0
  26. package/template/.claude/lib/session-frontmatter.test.mjs +242 -0
  27. package/template/.claude/recipes/migrate-from-notion.md +120 -0
  28. package/template/.claude/rules/agent-rules.md.skip +32 -0
  29. package/template/.claude/rules/cloud-infrastructure.md.skip +15 -0
  30. package/template/.claude/rules/coherent-revisions.md +24 -0
  31. package/template/.claude/rules/documentation.md.skip +13 -0
  32. package/template/.claude/rules/git-conventions.md +34 -0
  33. package/template/.claude/rules/honest-pushback.md +56 -0
  34. package/template/.claude/rules/local-dev-environment.md.skip +60 -0
  35. package/template/.claude/rules/memory-guidance.md +26 -0
  36. package/template/.claude/rules/product-integrity.md.skip +24 -0
  37. package/template/.claude/rules/scope-guard.md.skip +22 -0
  38. package/template/.claude/rules/superpowers-workflow.md.skip +22 -0
  39. package/template/.claude/rules/token-economics.md.skip +31 -0
  40. package/template/.claude/rules/work-item-tracking.md +90 -0
  41. package/template/.claude/rules/workspace-structure.md +69 -0
  42. package/template/.claude/scripts/add-repo-to-session.mjs +78 -0
  43. package/template/.claude/scripts/cleanup-work-session.mjs +108 -0
  44. package/template/.claude/scripts/create-work-session.mjs +124 -0
  45. package/template/.claude/scripts/migrate-open-work.mjs +91 -0
  46. package/template/.claude/scripts/migrate-session-layout.mjs +236 -0
  47. package/template/.claude/scripts/migrate-session-layout.test.mjs +144 -0
  48. package/template/.claude/scripts/trackers/github-issues.mjs +170 -0
  49. package/template/.claude/scripts/trackers/github-issues.test.mjs +190 -0
  50. package/template/.claude/scripts/trackers/interface.mjs +25 -0
  51. package/template/.claude/scripts/trackers/interface.test.mjs +40 -0
  52. package/template/.claude/settings.json +107 -0
  53. package/template/.claude/skills/aside/SKILL.md +125 -0
  54. package/template/.claude/skills/braindump/SKILL.md +96 -0
  55. package/template/.claude/skills/build-docs-site/SKILL.md +323 -0
  56. package/template/.claude/skills/build-docs-site/checklists/framing.md +221 -0
  57. package/template/.claude/skills/build-docs-site/checklists/pitfalls.md +228 -0
  58. package/template/.claude/skills/build-docs-site/checklists/review.md +130 -0
  59. package/template/.claude/skills/build-docs-site/scripts/bulk-fill-migration.py +393 -0
  60. package/template/.claude/skills/build-docs-site/scripts/forbidden-word-grep.mjs +159 -0
  61. package/template/.claude/skills/build-docs-site/scripts/leak-grep.mjs +328 -0
  62. package/template/.claude/skills/build-docs-site/templates/custom.css.tmpl +212 -0
  63. package/template/.claude/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +95 -0
  64. package/template/.claude/skills/build-docs-site/templates/primitives/Arrow.tsx +87 -0
  65. package/template/.claude/skills/build-docs-site/templates/primitives/Box.tsx +90 -0
  66. package/template/.claude/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +46 -0
  67. package/template/.claude/skills/build-docs-site/templates/primitives/Region.tsx +68 -0
  68. package/template/.claude/skills/build-docs-site/templates/primitives/SectionTitle.tsx +42 -0
  69. package/template/.claude/skills/build-docs-site/templates/primitives/tokens.ts +67 -0
  70. package/template/.claude/skills/build-docs-site/templates/sidebars.ts.tmpl +89 -0
  71. package/template/.claude/skills/build-docs-site/templates/spec.md.tmpl +119 -0
  72. package/template/.claude/skills/complete-work/SKILL.md +369 -0
  73. package/template/.claude/skills/handoff/SKILL.md +98 -0
  74. package/template/.claude/skills/maintenance/SKILL.md +116 -0
  75. package/template/.claude/skills/pause-work/SKILL.md +98 -0
  76. package/template/.claude/skills/promote/SKILL.md +77 -0
  77. package/template/.claude/skills/release/SKILL.md +126 -0
  78. package/template/.claude/skills/setup-tracker/SKILL.md +117 -0
  79. package/template/.claude/skills/start-work/SKILL.md +234 -0
  80. package/template/.claude/skills/sync-work/SKILL.md +73 -0
  81. package/template/.claude/skills/workspace-init/SKILL.md +420 -0
  82. package/template/.claude/skills/workspace-update/SKILL.md +108 -0
  83. package/template/.mcp.json +12 -0
  84. package/template/CLAUDE.md.tmpl +32 -0
  85. package/template/_gitignore +28 -0
  86. package/template/workspace.json.tmpl +15 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Myron Davis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/ukt-solutions/create-ulysses-workspace/main/docs/assets/logo.png" alt="Ulysses Workspace" width="220">
3
+ </p>
4
+
5
+ # @ulysses-ai/create-workspace
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@ulysses-ai/create-workspace.svg)](https://www.npmjs.com/package/@ulysses-ai/create-workspace)
8
+
9
+ > Rules, skills, and hooks that steer Claude Code through real work. Sessions you can pause and resume, multi-repo with versioning, shared context that survives chat boundaries.
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ # npm
15
+ npm create @ulysses-ai/workspace@latest
16
+
17
+ # yarn
18
+ yarn create @ulysses-ai/workspace
19
+
20
+ # pnpm
21
+ pnpm create @ulysses-ai/workspace
22
+
23
+ # bun
24
+ bun create @ulysses-ai/workspace
25
+ ```
26
+
27
+ Then:
28
+
29
+ ```bash
30
+ cd my-workspace
31
+ claude
32
+ /workspace-init
33
+ /start-work
34
+ ```
35
+
36
+ ## Why "Ulysses"?
37
+
38
+ Ulysses lashed himself to the mast so he could hear the Sirens without being steered into the rocks. A workspace does the same for Claude — freedom to do the work, constraints that keep it on course.
39
+
40
+ Rules say what's safe. Skills say how to do the recurring things. Hooks notice what would otherwise slip through. The combination is what gets you home.
41
+
42
+ ## What it gives you
43
+
44
+ Four things, in the order you'll touch them:
45
+
46
+ 1. **A workflow lifecycle that survives chat boundaries.** `/start-work` provisions a session — branch, worktree, tracker — atomically. `/pause-work` and `/sync-work` checkpoint mid-stream. `/complete-work` rebases, synthesizes release notes, opens PRs, and tears down. The same session resumes cleanly in a fresh chat.
47
+
48
+ 2. **Parallel work sessions you can run from separate terminals.** Each session lives in its own folder under `work-sessions/{name}/` with its own workspace worktree and nested project worktrees. Two sessions can't collide on a branch or a working directory.
49
+
50
+ 3. **Multi-repo support with versioning across repos.** A workspace wraps your project repos rather than replacing them. Each session can span one repo or many. `/release` synthesizes versioned release docs across the repos that contributed.
51
+
52
+ 4. **Shared context with a locked layer that stays in the window.** `shared-context/locked/` is loaded every turn and injected into subagents. Team truths arrive in the model's context window without anyone remembering to paste them.
53
+
54
+ > Everything Claude needs is in the file system. Everything a team shares is in git.
55
+
56
+ ## What you get
57
+
58
+ A scaffolded workspace with:
59
+
60
+ - **14 skills** covering the workflow lifecycle, releases, handoffs, and maintenance
61
+ - **6 active rules** + **8 optional `.skip` rules** for behaviors you can opt into
62
+ - **8 hooks** for SessionStart, SubagentStart, PreCompact, WorktreeCreate, and the rest of the small set the conventions rely on
63
+ - A **`shared-context/`** memory system with three visibility levels: locked (team truths), root (team-visible ephemerals), user-scoped (personal)
64
+ - Conventions for **multi-repo work sessions** with isolated git worktrees, parallelizable from separate terminals
65
+
66
+ (Counts are static prose, but a `prepublishOnly` audit script verifies them against the template at publish time — they don't drift.)
67
+
68
+ ## CLI
69
+
70
+ | Command | What it does |
71
+ | --- | --- |
72
+ | `npm create @ulysses-ai/workspace@latest` | Interactive scaffolder (recommended) |
73
+ | `npx @ulysses-ai/create-workspace --init [dir]` | Non-interactive fresh install (pass dir directly) |
74
+ | `npx @ulysses-ai/create-workspace --upgrade [dir]` | Apply template updates to an existing workspace |
75
+
76
+ > **Why two forms?** `npm create <pkg>` resolves to `npx create-<pkg>`, but it consumes the `--init` flag for itself (npm's own subcommand alias). Use the bare `npm create` form for interactive scaffolding; use `npx` directly when you want to pass `--init <dir>` non-interactively.
77
+
78
+ ## Documentation
79
+
80
+ Start here:
81
+
82
+ - **[Solo Developer Guide](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/guides/solo-developer.md)** — recommended starting point
83
+ - **[Team Lead Guide](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/guides/team-lead.md)**
84
+ - **[New Team Member Guide](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/guides/new-team-member.md)**
85
+
86
+ The eleven [chapters](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/) cover the model in depth — concepts, the toolkit, the release lifecycle, behavioral patterns:
87
+
88
+ | Part | Chapter | Topic |
89
+ |------|---------|-------|
90
+ | Concepts | [01](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/01-what-is-a-workspace.md) | What Is a Workspace |
91
+ | | [02](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/02-work-sessions.md) | Work Sessions |
92
+ | | [03](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/03-shared-context.md) | Shared Context |
93
+ | The Toolkit | [04](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/04-claude-md.md) | CLAUDE.md |
94
+ | | [05](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/05-rules.md) | Rules |
95
+ | | [06](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/06-skills.md) | Skills |
96
+ | | [07](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/07-hooks-and-scripts.md) | Hooks and Scripts |
97
+ | | [08](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/08-agents.md) | Agents |
98
+ | Lifecycle | [09](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/09-the-release-cycle.md) | The Release Cycle |
99
+ | | [10](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/10-installation-and-upgrades.md) | Installation and Upgrades |
100
+ | Practice | [11](https://github.com/ukt-solutions/create-ulysses-workspace/blob/main/docs/chapters/11-behavioral-patterns.md) | Behavioral Patterns |
101
+
102
+ ## Status
103
+
104
+ In active pre-1.0 development. Used as dogfood and validated against external workspaces. Conventions and CLI flags are stable; small refinements continue. v1.0 will mark a stability commitment.
105
+
106
+ ## License
107
+
108
+ MIT
package/bin/create.mjs ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+
3
+ const [major, minor] = process.versions.node.split('.').map(Number);
4
+ if (major < 20 || (major === 20 && minor < 9)) {
5
+ console.error(`@ulysses-ai/create-workspace requires Node.js 20.9 or later.`);
6
+ console.error(` You have: ${process.versions.node}`);
7
+ console.error(` Required: >=20.9.0`);
8
+ console.error(` Try installing a newer Node via nvm, fnm, or the Node website.`);
9
+ process.exit(1);
10
+ }
11
+
12
+ import { runPrompts } from '../lib/prompts.mjs';
13
+ import { scaffold } from '../lib/scaffold.mjs';
14
+ import { initGit, cloneRepos } from '../lib/git.mjs';
15
+ import { resolve } from 'path';
16
+
17
+ async function main() {
18
+ console.log('\n Ulysses Workspace\n');
19
+
20
+ const initFlag = process.argv.includes('--init');
21
+ const upgradeFlag = process.argv.includes('--upgrade');
22
+ const migrateFlag = process.argv.includes('--migrate');
23
+
24
+ // Deprecated --migrate
25
+ if (migrateFlag) {
26
+ console.error(' --migrate is deprecated. Use --init (fresh install) or --upgrade (template update).');
27
+ console.error('');
28
+ console.error(' Fresh install: npx @ulysses-ai/create-workspace --init [target-dir]');
29
+ console.error(' Template update: npx @ulysses-ai/create-workspace --upgrade [target-dir]');
30
+ console.error('');
31
+ process.exit(1);
32
+ }
33
+
34
+ if (initFlag) {
35
+ const { initWorkspace } = await import('../lib/init.mjs');
36
+ const args = process.argv.slice(process.argv.indexOf('--init') + 1);
37
+ const targetDir = resolve(args.find(a => !a.startsWith('--')) || '.');
38
+ await initWorkspace(targetDir);
39
+ return;
40
+ }
41
+
42
+ if (upgradeFlag) {
43
+ const { upgradeWorkspace } = await import('../lib/upgrade.mjs');
44
+ const args = process.argv.slice(process.argv.indexOf('--upgrade') + 1);
45
+ const targetDir = resolve(args.find(a => !a.startsWith('--')) || '.');
46
+ await upgradeWorkspace(targetDir);
47
+ return;
48
+ }
49
+
50
+ // Default: interactive scaffold
51
+ const answers = await runPrompts();
52
+
53
+ if (!answers) {
54
+ console.log('Setup cancelled.');
55
+ process.exit(0);
56
+ }
57
+
58
+ const workspacePath = await scaffold(answers);
59
+ await initGit(workspacePath);
60
+
61
+ if (answers.repos.length > 0) {
62
+ await cloneRepos(workspacePath, answers.repos);
63
+ }
64
+
65
+ console.log(`
66
+ \u2713 Workspace created at ${workspacePath}
67
+ \u2713 Git repo initialized${answers.repos.length > 0 ? `\n \u2713 ${answers.repos.length} repo(s) cloned to repos/` : ''}
68
+
69
+ Next steps:
70
+ cd ${workspacePath}
71
+ claude
72
+ /start-work blank
73
+ `);
74
+ }
75
+
76
+ main().catch((err) => {
77
+ console.error('Error:', err.message);
78
+ process.exit(1);
79
+ });
package/lib/git.mjs ADDED
@@ -0,0 +1,26 @@
1
+ import { execSync } from 'child_process';
2
+ import { join } from 'path';
3
+
4
+ export async function initGit(workspacePath) {
5
+ execSync('git init', { cwd: workspacePath, stdio: 'pipe' });
6
+ execSync('git add -A', { cwd: workspacePath, stdio: 'pipe' });
7
+ execSync('git commit -m "chore: initialize claude-workspace"', {
8
+ cwd: workspacePath,
9
+ stdio: 'pipe',
10
+ });
11
+ }
12
+
13
+ export async function cloneRepos(workspacePath, repos) {
14
+ const reposDir = join(workspacePath, 'repos');
15
+
16
+ for (const repo of repos) {
17
+ const targetDir = join(reposDir, repo.name);
18
+ console.log(` Cloning ${repo.name}...`);
19
+ try {
20
+ execSync(`git clone "${repo.remote}" "${targetDir}"`, { stdio: 'pipe' });
21
+ } catch (err) {
22
+ console.warn(` \u26A0 Failed to clone ${repo.name}: ${err.message}`);
23
+ console.warn(` You can clone it manually later or run /setup in Claude Code.`);
24
+ }
25
+ }
26
+ }
package/lib/init.mjs ADDED
@@ -0,0 +1,129 @@
1
+ // lib/init.mjs
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, readdirSync, statSync } from 'fs';
3
+ import { join, basename } from 'path';
4
+ import { stagePayload } from './payload.mjs';
5
+
6
+ function ensureDir(dir) {
7
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
8
+ }
9
+
10
+ export async function initWorkspace(targetDir) {
11
+ const name = basename(targetDir);
12
+ const workspaceJsonPath = join(targetDir, 'workspace.json');
13
+ const claudeMdPath = join(targetDir, 'CLAUDE.md');
14
+ const gitignorePath = join(targetDir, '.gitignore');
15
+
16
+ console.log(`\n @ulysses-ai/create-workspace --init`);
17
+ console.log(` Target: ${targetDir}\n`);
18
+
19
+ // Stage payload
20
+ const { toVersion, payloadDir } = stagePayload(targetDir, { action: 'init' });
21
+ console.log(` Staged template payload (v${toVersion})`);
22
+
23
+ // Install only the bootstrap skills needed to complete initialization
24
+ const bootstrapSkills = ['workspace-init', 'workspace-update'];
25
+ const payloadSkills = join(payloadDir, '.claude', 'skills');
26
+ const targetSkills = join(targetDir, '.claude', 'skills');
27
+ for (const skill of bootstrapSkills) {
28
+ const src = join(payloadSkills, skill);
29
+ const dest = join(targetSkills, skill);
30
+ if (existsSync(src) && !existsSync(dest)) {
31
+ ensureDir(dest);
32
+ cpSync(src, dest, { recursive: true });
33
+ }
34
+ }
35
+ console.log(' Installed bootstrap skills (workspace-init, workspace-update)');
36
+
37
+ // Install hooks, scripts, and lib (needed for workspace to function)
38
+ for (const dir of ['hooks', 'scripts', 'lib']) {
39
+ const src = join(payloadDir, '.claude', dir);
40
+ const dest = join(targetDir, '.claude', dir);
41
+ if (!existsSync(src)) continue;
42
+ ensureDir(dest);
43
+ for (const entry of readdirSync(src)) {
44
+ const srcEntry = join(src, entry);
45
+ const destEntry = join(dest, entry);
46
+ if (!existsSync(destEntry) && !statSync(srcEntry).isDirectory()) {
47
+ cpSync(srcEntry, destEntry);
48
+ }
49
+ }
50
+ console.log(` Installed ${dir}`);
51
+ }
52
+
53
+ // Create shared-context structure. We always want shared-context/ and
54
+ // shared-context/locked/ to exist after init, even if the payload ships
55
+ // no files for them yet — locked/ is referenced by CLAUDE.md.tmpl via
56
+ // the @shared-context/locked/ import directive.
57
+ const payloadContext = join(payloadDir, 'shared-context');
58
+ const targetContext = join(targetDir, 'shared-context');
59
+ if (existsSync(payloadContext) && !existsSync(targetContext)) {
60
+ cpSync(payloadContext, targetContext, { recursive: true });
61
+ console.log(' Created shared-context/');
62
+ }
63
+ ensureDir(join(targetDir, 'shared-context', 'locked'));
64
+
65
+ // Everything else (repos/, work-sessions/, workspace-scratchpad/) is
66
+ // lazy-created by scripts and hooks when they first need to write.
67
+ // We intentionally do NOT pre-create these dirs — they get made on demand.
68
+
69
+ // Create workspace.json if missing
70
+ if (!existsSync(workspaceJsonPath)) {
71
+ writeFileSync(workspaceJsonPath, JSON.stringify({
72
+ workspace: {
73
+ name,
74
+ templateVersion: toVersion,
75
+ scratchpadDir: 'workspace-scratchpad',
76
+ workSessionsDir: 'work-sessions',
77
+ sharedContextDir: 'shared-context',
78
+ releaseNotesDir: 'release-notes',
79
+ subagentContextMaxBytes: 10240,
80
+ greeting: `Welcome back to ${name}.`,
81
+ },
82
+ repos: {},
83
+ }, null, 2) + '\n');
84
+ console.log(' Created workspace.json');
85
+ }
86
+
87
+ // Replace CLAUDE.md with template version (back up existing)
88
+ const tmplPath = join(payloadDir, 'CLAUDE.md.tmpl');
89
+ let claudeContent;
90
+ if (existsSync(tmplPath)) {
91
+ claudeContent = readFileSync(tmplPath, 'utf-8').replace(/\{\{project-name\}\}/g, name);
92
+ } else {
93
+ claudeContent = `## Workspace: ${name}\n\nThis is a claude-workspace. All conventions are defined in .claude/rules/.\n`;
94
+ }
95
+
96
+ if (existsSync(claudeMdPath)) {
97
+ cpSync(claudeMdPath, claudeMdPath + '.bak');
98
+ console.log(' Backed up existing CLAUDE.md to CLAUDE.md.bak');
99
+ }
100
+ writeFileSync(claudeMdPath, claudeContent);
101
+ console.log(' Created CLAUDE.md');
102
+
103
+ // Set up .gitignore
104
+ const payloadGitignore = join(payloadDir, '_gitignore');
105
+ if (existsSync(payloadGitignore)) {
106
+ if (existsSync(gitignorePath)) {
107
+ const existing = readFileSync(gitignorePath, 'utf-8');
108
+ const template = readFileSync(payloadGitignore, 'utf-8');
109
+ const existingLines = new Set(existing.split('\n').map(l => l.trim()));
110
+ const newLines = template.split('\n').filter(l => l.trim() && !existingLines.has(l.trim()));
111
+ if (newLines.length > 0) {
112
+ writeFileSync(gitignorePath, existing.trimEnd() + '\n\n# From workspace template\n' + newLines.join('\n') + '\n');
113
+ console.log(' Merged template entries into .gitignore');
114
+ }
115
+ } else {
116
+ cpSync(payloadGitignore, gitignorePath);
117
+ console.log(' Created .gitignore');
118
+ }
119
+ }
120
+
121
+ console.log(`
122
+ Workspace initialized (v${toVersion}).
123
+
124
+ Next steps:
125
+ cd ${targetDir}
126
+ claude
127
+ /workspace-init
128
+ `);
129
+ }
@@ -0,0 +1,44 @@
1
+ // lib/payload.mjs
2
+ import { existsSync, cpSync, rmSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const TEMPLATE_DIR = join(__dirname, '..', 'template');
8
+
9
+ export function getTemplateVersion() {
10
+ return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')).version;
11
+ }
12
+
13
+ export function stagePayload(targetDir, { action, fromVersion = null }) {
14
+ const payloadDir = join(targetDir, '.workspace-update');
15
+ const toVersion = getTemplateVersion();
16
+
17
+ // Clean any existing payload
18
+ if (existsSync(payloadDir)) {
19
+ rmSync(payloadDir, { recursive: true });
20
+ }
21
+
22
+ // Copy template to payload directory
23
+ cpSync(TEMPLATE_DIR, payloadDir, { recursive: true });
24
+
25
+ // Write manifest
26
+ const manifest = {
27
+ action,
28
+ templateVersion: toVersion,
29
+ timestamp: new Date().toISOString(),
30
+ source: '@ulysses-ai/create-workspace',
31
+ };
32
+ if (fromVersion) manifest.fromVersion = fromVersion;
33
+
34
+ writeFileSync(join(payloadDir, '.manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
35
+
36
+ return { payloadDir, toVersion, fromVersion };
37
+ }
38
+
39
+ export function cleanPayload(targetDir) {
40
+ const payloadDir = join(targetDir, '.workspace-update');
41
+ if (existsSync(payloadDir)) {
42
+ rmSync(payloadDir, { recursive: true });
43
+ }
44
+ }
@@ -0,0 +1,113 @@
1
+ import prompts from 'prompts';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+
5
+ export async function runPrompts() {
6
+ const onCancel = () => {
7
+ return false;
8
+ };
9
+
10
+ // Workspace name
11
+ const { name } = await prompts({
12
+ type: 'text',
13
+ name: 'name',
14
+ message: 'Workspace name:',
15
+ validate: (v) => (v.trim() ? true : 'Name is required'),
16
+ }, { onCancel });
17
+ if (!name) return null;
18
+
19
+ // Directory
20
+ const defaultDir = join(homedir(), 'claude-workspaces', name);
21
+ const { directory } = await prompts({
22
+ type: 'text',
23
+ name: 'directory',
24
+ message: 'Directory:',
25
+ initial: defaultDir,
26
+ }, { onCancel });
27
+ if (!directory) return null;
28
+
29
+ // Repos
30
+ const repos = [];
31
+ let addMore = true;
32
+ while (addMore) {
33
+ const { addRepo } = await prompts({
34
+ type: 'confirm',
35
+ name: 'addRepo',
36
+ message: repos.length === 0 ? 'Add a repository?' : 'Add another repository?',
37
+ initial: repos.length === 0,
38
+ }, { onCancel });
39
+ if (addRepo === undefined) return null;
40
+
41
+ if (!addRepo) {
42
+ addMore = false;
43
+ break;
44
+ }
45
+
46
+ const repoAnswers = await prompts([
47
+ {
48
+ type: 'text',
49
+ name: 'remote',
50
+ message: ' Remote URL:',
51
+ validate: (v) => (v.trim() ? true : 'URL is required'),
52
+ },
53
+ {
54
+ type: 'text',
55
+ name: 'branch',
56
+ message: ' Default branch:',
57
+ initial: 'main',
58
+ },
59
+ {
60
+ type: 'confirm',
61
+ name: 'primary',
62
+ message: ' Primary repo?',
63
+ initial: repos.length === 0,
64
+ },
65
+ ], { onCancel });
66
+ if (!repoAnswers.remote) return null;
67
+
68
+ // Extract repo name from remote URL
69
+ const repoName = repoAnswers.remote
70
+ .split('/')
71
+ .pop()
72
+ .replace(/\.git$/, '');
73
+
74
+ repos.push({ name: repoName, ...repoAnswers });
75
+ }
76
+
77
+ // User name
78
+ const defaultUser = process.env.USER || process.env.USERNAME || 'user';
79
+ const { userName } = await prompts({
80
+ type: 'text',
81
+ name: 'userName',
82
+ message: 'Your name (for handoff scoping):',
83
+ initial: defaultUser,
84
+ }, { onCancel });
85
+ if (!userName) return null;
86
+
87
+ // Optional rules
88
+ const skipRules = [
89
+ { title: 'cloud-infrastructure', value: 'cloud-infrastructure' },
90
+ { title: 'superpowers-workflow', value: 'superpowers-workflow' },
91
+ { title: 'documentation', value: 'documentation' },
92
+ { title: 'scope-guard', value: 'scope-guard' },
93
+ { title: 'token-economics', value: 'token-economics' },
94
+ { title: 'agent-rules', value: 'agent-rules' },
95
+ ];
96
+
97
+ const { activateRules } = await prompts({
98
+ type: 'multiselect',
99
+ name: 'activateRules',
100
+ message: 'Activate optional rules:',
101
+ choices: skipRules,
102
+ hint: '- Space to select, Enter to confirm',
103
+ }, { onCancel });
104
+ if (!activateRules) return null;
105
+
106
+ return {
107
+ name,
108
+ directory,
109
+ repos,
110
+ userName,
111
+ activateRules,
112
+ };
113
+ }
@@ -0,0 +1,84 @@
1
+ import { cpSync, mkdirSync, readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const TEMPLATE_DIR = join(__dirname, '..', 'template');
7
+
8
+ export async function scaffold(answers) {
9
+ const { name, directory, repos, userName, activateRules } = answers;
10
+
11
+ // Create target directory
12
+ mkdirSync(directory, { recursive: true });
13
+
14
+ // Copy template files
15
+ cpSync(TEMPLATE_DIR, directory, {
16
+ recursive: true,
17
+ filter: (src) => {
18
+ // Skip .tmpl files — we'll process them separately
19
+ return !src.endsWith('.tmpl');
20
+ },
21
+ });
22
+
23
+ // Ensure shared-context/locked/ exists so the CLAUDE.md import resolves.
24
+ // repos/, work-sessions/, and workspace-scratchpad/ are lazy-created when
25
+ // scripts and hooks first need them — we do NOT pre-create them here.
26
+ mkdirSync(join(directory, 'shared-context', 'locked'), { recursive: true });
27
+
28
+ // Rename _gitignore to .gitignore
29
+ const gitignoreSrc = join(directory, '_gitignore');
30
+ const gitignoreDest = join(directory, '.gitignore');
31
+ if (existsSync(gitignoreSrc)) {
32
+ renameSync(gitignoreSrc, gitignoreDest);
33
+ }
34
+
35
+ // Process CLAUDE.md template
36
+ const claudeMdTmpl = readFileSync(join(TEMPLATE_DIR, 'CLAUDE.md.tmpl'), 'utf-8');
37
+ const claudeMd = claudeMdTmpl.replace(/\{\{project-name\}\}/g, name);
38
+ writeFileSync(join(directory, 'CLAUDE.md'), claudeMd);
39
+
40
+ // Process workspace.json template
41
+ const workspaceJsonTmpl = readFileSync(join(TEMPLATE_DIR, 'workspace.json.tmpl'), 'utf-8');
42
+ const workspaceConfig = JSON.parse(workspaceJsonTmpl.replace(/\{\{project-name\}\}/g, name));
43
+
44
+ // Stamp template version
45
+ const pkgJson = JSON.parse(readFileSync(join(TEMPLATE_DIR, '..', 'package.json'), 'utf-8'));
46
+ workspaceConfig.workspace.templateVersion = pkgJson.version;
47
+
48
+ // Populate repos
49
+ for (const repo of repos) {
50
+ workspaceConfig.repos[repo.name] = {
51
+ remote: repo.remote,
52
+ branch: repo.branch,
53
+ };
54
+ if (repo.primary) {
55
+ workspaceConfig.repos[repo.name].primary = true;
56
+ }
57
+ }
58
+ writeFileSync(
59
+ join(directory, 'workspace.json'),
60
+ JSON.stringify(workspaceConfig, null, 2) + '\n'
61
+ );
62
+
63
+ // Write settings.local.json with user identity
64
+ const settingsLocal = {
65
+ workspace: {
66
+ user: userName,
67
+ },
68
+ };
69
+ writeFileSync(
70
+ join(directory, '.claude', 'settings.local.json'),
71
+ JSON.stringify(settingsLocal, null, 2) + '\n'
72
+ );
73
+
74
+ // Activate selected optional rules (rename .md.skip → .md)
75
+ for (const rule of activateRules) {
76
+ const skipPath = join(directory, '.claude', 'rules', `${rule}.md.skip`);
77
+ const activePath = join(directory, '.claude', 'rules', `${rule}.md`);
78
+ if (existsSync(skipPath)) {
79
+ renameSync(skipPath, activePath);
80
+ }
81
+ }
82
+
83
+ return directory;
84
+ }
@@ -0,0 +1,42 @@
1
+ // lib/upgrade.mjs
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { stagePayload } from './payload.mjs';
5
+
6
+ export async function upgradeWorkspace(targetDir) {
7
+ const workspaceJsonPath = join(targetDir, 'workspace.json');
8
+
9
+ console.log(`\n @ulysses-ai/create-workspace --upgrade`);
10
+ console.log(` Target: ${targetDir}\n`);
11
+
12
+ // Verify workspace exists and is initialized
13
+ if (!existsSync(workspaceJsonPath)) {
14
+ console.error(` Error: No workspace.json found at ${targetDir}.`);
15
+ console.error(` Run with --init instead:\n npx @ulysses-ai/create-workspace --init ${targetDir}\n`);
16
+ process.exit(1);
17
+ }
18
+
19
+ const config = JSON.parse(readFileSync(workspaceJsonPath, 'utf-8'));
20
+ const initialized = config.workspace?.initialized || config.workspace?.templateVersion;
21
+ if (!initialized) {
22
+ console.error(` Error: Workspace not initialized.`);
23
+ console.error(` Run with --init instead:\n npx @ulysses-ai/create-workspace --init ${targetDir}\n`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const fromVersion = config.workspace?.templateVersion || 'unknown';
28
+
29
+ // Stage payload
30
+ const { toVersion } = stagePayload(targetDir, { action: 'upgrade', fromVersion });
31
+
32
+ if (fromVersion === toVersion) {
33
+ console.log(` Workspace is already on template v${toVersion}.`);
34
+ console.log(` Payload staged anyway — run /workspace-update to verify integrity.\n`);
35
+ } else {
36
+ console.log(` Staged template payload (v${fromVersion} → v${toVersion})`);
37
+ }
38
+
39
+ console.log(` Template payload staged. The workspace will update on your
40
+ next Claude Code prompt.
41
+ `);
42
+ }