@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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/bin/create.mjs +79 -0
- package/lib/git.mjs +26 -0
- package/lib/init.mjs +129 -0
- package/lib/payload.mjs +44 -0
- package/lib/prompts.mjs +113 -0
- package/lib/scaffold.mjs +84 -0
- package/lib/upgrade.mjs +42 -0
- package/package.json +43 -0
- package/template/.claude/agents/aside-researcher.md +48 -0
- package/template/.claude/agents/implementer.md +39 -0
- package/template/.claude/agents/researcher.md +40 -0
- package/template/.claude/agents/reviewer.md +47 -0
- package/template/.claude/hooks/_utils.mjs +196 -0
- package/template/.claude/hooks/_utils.test.mjs +99 -0
- package/template/.claude/hooks/post-compact.mjs +7 -0
- package/template/.claude/hooks/pre-compact.mjs +34 -0
- package/template/.claude/hooks/repo-write-detection.mjs +107 -0
- package/template/.claude/hooks/session-end.mjs +91 -0
- package/template/.claude/hooks/session-start.mjs +150 -0
- package/template/.claude/hooks/subagent-start.mjs +44 -0
- package/template/.claude/hooks/workspace-update-check.mjs +42 -0
- package/template/.claude/hooks/worktree-create.mjs +53 -0
- package/template/.claude/lib/session-frontmatter.mjs +265 -0
- package/template/.claude/lib/session-frontmatter.test.mjs +242 -0
- package/template/.claude/recipes/migrate-from-notion.md +120 -0
- package/template/.claude/rules/agent-rules.md.skip +32 -0
- package/template/.claude/rules/cloud-infrastructure.md.skip +15 -0
- package/template/.claude/rules/coherent-revisions.md +24 -0
- package/template/.claude/rules/documentation.md.skip +13 -0
- package/template/.claude/rules/git-conventions.md +34 -0
- package/template/.claude/rules/honest-pushback.md +56 -0
- package/template/.claude/rules/local-dev-environment.md.skip +60 -0
- package/template/.claude/rules/memory-guidance.md +26 -0
- package/template/.claude/rules/product-integrity.md.skip +24 -0
- package/template/.claude/rules/scope-guard.md.skip +22 -0
- package/template/.claude/rules/superpowers-workflow.md.skip +22 -0
- package/template/.claude/rules/token-economics.md.skip +31 -0
- package/template/.claude/rules/work-item-tracking.md +90 -0
- package/template/.claude/rules/workspace-structure.md +69 -0
- package/template/.claude/scripts/add-repo-to-session.mjs +78 -0
- package/template/.claude/scripts/cleanup-work-session.mjs +108 -0
- package/template/.claude/scripts/create-work-session.mjs +124 -0
- package/template/.claude/scripts/migrate-open-work.mjs +91 -0
- package/template/.claude/scripts/migrate-session-layout.mjs +236 -0
- package/template/.claude/scripts/migrate-session-layout.test.mjs +144 -0
- package/template/.claude/scripts/trackers/github-issues.mjs +170 -0
- package/template/.claude/scripts/trackers/github-issues.test.mjs +190 -0
- package/template/.claude/scripts/trackers/interface.mjs +25 -0
- package/template/.claude/scripts/trackers/interface.test.mjs +40 -0
- package/template/.claude/settings.json +107 -0
- package/template/.claude/skills/aside/SKILL.md +125 -0
- package/template/.claude/skills/braindump/SKILL.md +96 -0
- package/template/.claude/skills/build-docs-site/SKILL.md +323 -0
- package/template/.claude/skills/build-docs-site/checklists/framing.md +221 -0
- package/template/.claude/skills/build-docs-site/checklists/pitfalls.md +228 -0
- package/template/.claude/skills/build-docs-site/checklists/review.md +130 -0
- package/template/.claude/skills/build-docs-site/scripts/bulk-fill-migration.py +393 -0
- package/template/.claude/skills/build-docs-site/scripts/forbidden-word-grep.mjs +159 -0
- package/template/.claude/skills/build-docs-site/scripts/leak-grep.mjs +328 -0
- package/template/.claude/skills/build-docs-site/templates/custom.css.tmpl +212 -0
- package/template/.claude/skills/build-docs-site/templates/docusaurus.config.ts.tmpl +95 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/Arrow.tsx +87 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/Box.tsx +90 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/DiagramContainer.tsx +46 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/Region.tsx +68 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/SectionTitle.tsx +42 -0
- package/template/.claude/skills/build-docs-site/templates/primitives/tokens.ts +67 -0
- package/template/.claude/skills/build-docs-site/templates/sidebars.ts.tmpl +89 -0
- package/template/.claude/skills/build-docs-site/templates/spec.md.tmpl +119 -0
- package/template/.claude/skills/complete-work/SKILL.md +369 -0
- package/template/.claude/skills/handoff/SKILL.md +98 -0
- package/template/.claude/skills/maintenance/SKILL.md +116 -0
- package/template/.claude/skills/pause-work/SKILL.md +98 -0
- package/template/.claude/skills/promote/SKILL.md +77 -0
- package/template/.claude/skills/release/SKILL.md +126 -0
- package/template/.claude/skills/setup-tracker/SKILL.md +117 -0
- package/template/.claude/skills/start-work/SKILL.md +234 -0
- package/template/.claude/skills/sync-work/SKILL.md +73 -0
- package/template/.claude/skills/workspace-init/SKILL.md +420 -0
- package/template/.claude/skills/workspace-update/SKILL.md +108 -0
- package/template/.mcp.json +12 -0
- package/template/CLAUDE.md.tmpl +32 -0
- package/template/_gitignore +28 -0
- 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
|
+
[](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
|
+
}
|
package/lib/payload.mjs
ADDED
|
@@ -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
|
+
}
|
package/lib/prompts.mjs
ADDED
|
@@ -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
|
+
}
|
package/lib/scaffold.mjs
ADDED
|
@@ -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
|
+
}
|
package/lib/upgrade.mjs
ADDED
|
@@ -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
|
+
}
|