@dezkareid/osddt 1.10.1 → 1.11.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/AGENTS.md +78 -35
- package/README.md +70 -21
- package/dist/commands/start-worktree.d.ts +9 -0
- package/dist/commands/worktree-info.d.ts +2 -0
- package/dist/index.js +449 -27
- package/dist/utils/prompt.d.ts +1 -0
- package/dist/utils/worktree.d.ts +11 -0
- package/package.json +4 -4
- package/dist/commands/context.spec.d.ts +0 -1
- package/dist/commands/done.spec.d.ts +0 -1
- package/dist/commands/meta-info.spec.d.ts +0 -1
- package/dist/commands/setup.spec.d.ts +0 -1
- package/dist/commands/update.spec.d.ts +0 -1
- package/dist/templates/claude.spec.d.ts +0 -1
- package/dist/templates/gemini.spec.d.ts +0 -1
- package/dist/templates/shared.spec.d.ts +0 -1
package/AGENTS.md
CHANGED
|
@@ -82,7 +82,6 @@ Always prefer use exact versions for dependencies. Do not use `^` or `~`.
|
|
|
82
82
|
|
|
83
83
|
#### Linting & Formatting
|
|
84
84
|
- **ESLint**: `9.39.2`
|
|
85
|
-
- **Prettier**: `3.8.1`
|
|
86
85
|
|
|
87
86
|
#### Type Definitions
|
|
88
87
|
- **@types/node**: `25.0.10`
|
|
@@ -112,7 +111,8 @@ osddt/
|
|
|
112
111
|
│ │ ├── claude.ts # Formats COMMAND_DEFINITIONS as Markdown (.claude/commands/osddt.<name>.md)
|
|
113
112
|
│ │ └── gemini.ts # Formats COMMAND_DEFINITIONS as TOML (.gemini/commands/osddt.<name>.toml)
|
|
114
113
|
│ └── utils/
|
|
115
|
-
│
|
|
114
|
+
│ ├── prompt.ts # Inquirer prompts: askAgents() (checkbox), askRepoType() (select), askWorktreeUrl() (input)
|
|
115
|
+
│ └── worktree.ts # Shared worktree helpers: git version check, not-a-worktree check, writable check, state file init
|
|
116
116
|
├── dist/ # Compiled output (single ESM bundle, gitignored)
|
|
117
117
|
├── rollup.config.js # Bundles src/index.ts → dist/index.js (ESM, shebang injected)
|
|
118
118
|
├── tsconfig.json # TypeScript config
|
|
@@ -126,7 +126,7 @@ osddt/
|
|
|
126
126
|
#### Key relationships
|
|
127
127
|
|
|
128
128
|
- **`shared.ts` is the single source of truth** for all command template content. Both `claude.ts` and `gemini.ts` import `COMMAND_DEFINITIONS` from it and only differ in file format (Markdown vs TOML) and argument placeholder (`$ARGUMENTS` vs `{{args}}`).
|
|
129
|
-
- **`setup.ts`** orchestrates setup: reads `--agents`/`--repo-type` flags when provided (non-interactive), otherwise calls `askAgents()` → `askRepoType()` → writes selected agent files → writes `.osddtrc`.
|
|
129
|
+
- **`setup.ts`** orchestrates setup: reads `--agents`/`--repo-type`/`--worktree-repository` flags when provided (non-interactive), otherwise calls `askAgents()` → `askRepoType()` → `askWorktreeUrl()` → if URL provided, runs worktree environment checks via `worktree.ts` → writes selected agent files → writes `.osddtrc`.
|
|
130
130
|
- **`meta-info.ts`** is referenced inside the generated templates so agents can fetch live branch/date at invocation time (not baked in at build time).
|
|
131
131
|
- **Test files** (`.spec.ts`) live next to the source file they cover. Template tests are pure (no mocks); command tests mock `fs-extra` and `child_process`.
|
|
132
132
|
|
|
@@ -147,18 +147,28 @@ The selected agents are saved in `.osddtrc` alongside `repoType`. When `osddt up
|
|
|
147
147
|
|
|
148
148
|
#### Available commands
|
|
149
149
|
|
|
150
|
-
| Command
|
|
151
|
-
|
|
|
152
|
-
| `osddt setup`
|
|
153
|
-
| `osddt setup --agents <list> --repo-type <type>`
|
|
154
|
-
| `
|
|
155
|
-
| `npx @dezkareid/osddt setup
|
|
156
|
-
| `osddt
|
|
157
|
-
| `npx @dezkareid/osddt
|
|
158
|
-
| `osddt
|
|
159
|
-
| `npx @dezkareid/osddt
|
|
160
|
-
| `osddt
|
|
161
|
-
| `npx @dezkareid/osddt
|
|
150
|
+
| Command | Context | Description |
|
|
151
|
+
| ---------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------------------- |
|
|
152
|
+
| `osddt setup` | Local dev | Generate agent command files for Claude and Gemini |
|
|
153
|
+
| `osddt setup --agents <list> --repo-type <type>` | Local dev | Non-interactive setup (for CI/scripted environments) |
|
|
154
|
+
| `osddt setup --worktree-repository <url>` | Local dev | Setup with worktree workflow enabled |
|
|
155
|
+
| `npx @dezkareid/osddt setup` | External | Generate agent command files for Claude and Gemini |
|
|
156
|
+
| `npx @dezkareid/osddt setup --agents <list> --repo-type <type>` | External | Non-interactive setup (for CI/scripted environments) |
|
|
157
|
+
| `npx @dezkareid/osddt setup --worktree-repository <url>` | External | Setup with worktree workflow enabled |
|
|
158
|
+
| `osddt meta-info` | Local dev | Output current branch and date as JSON |
|
|
159
|
+
| `npx @dezkareid/osddt meta-info` | External | Output current branch and date as JSON |
|
|
160
|
+
| `osddt done <feature-name> --dir <project-path>` | Local dev | Move `working-on/<feature>` to `done/<feature>` |
|
|
161
|
+
| `npx @dezkareid/osddt done <feature-name> --dir <project-path>` | External | Move `working-on/<feature>` to `done/<feature>` |
|
|
162
|
+
| `osddt done <feature-name> --dir <project-path> --worktree` | Local dev | Archive feature, remove git worktree, and clean state file |
|
|
163
|
+
| `npx @dezkareid/osddt done <feature-name> --dir <project-path> --worktree` | External | Archive feature, remove git worktree, and clean state file |
|
|
164
|
+
| `osddt update` | Local dev | Regenerate agent command files from the existing `.osddtrc` |
|
|
165
|
+
| `npx @dezkareid/osddt update` | External | Regenerate agent command files from the existing `.osddtrc` |
|
|
166
|
+
| `osddt start-worktree <feature-name>` | Local dev | Create a git worktree for a feature and scaffold working-on/ |
|
|
167
|
+
| `npx @dezkareid/osddt start-worktree <feature-name>` | External | Create a git worktree for a feature and scaffold working-on/ |
|
|
168
|
+
| `osddt start-worktree <feature-name> --dir <package-path>` | Local dev | Same, specifying the package path in a monorepo |
|
|
169
|
+
| `npx @dezkareid/osddt start-worktree <feature-name> --dir <package-path>` | External | Same, specifying the package path in a monorepo |
|
|
170
|
+
| `osddt worktree-info <feature-name>` | Local dev | Look up worktree paths for a feature (JSON output) |
|
|
171
|
+
| `npx @dezkareid/osddt worktree-info <feature-name>` | External | Look up worktree paths for a feature (JSON output) |
|
|
162
172
|
|
|
163
173
|
#### `osddt setup` options
|
|
164
174
|
|
|
@@ -166,26 +176,27 @@ The selected agents are saved in `.osddtrc` alongside `repoType`. When `osddt up
|
|
|
166
176
|
| ---- | ------ | ----------- |
|
|
167
177
|
| `--agents <list>` | `claude`, `gemini` (comma-separated) | Skip the agents prompt and use the provided value(s) |
|
|
168
178
|
| `--repo-type <type>` | `single`, `monorepo` | Skip the repo type prompt and use the provided value |
|
|
179
|
+
| `--worktree-repository <url>` | any git URL | Enable worktree workflow; saves URL as `"worktree-repository"` in `.osddtrc` |
|
|
169
180
|
| `-d, --dir <directory>` | any path | Target directory (defaults to current working directory) |
|
|
170
181
|
|
|
171
|
-
|
|
182
|
+
All flags are optional. Providing neither runs the fully interactive mode. When `--worktree-repository` is provided, environment checks run and `.osddt-worktrees` is initialised.
|
|
172
183
|
|
|
173
184
|
### Command Templates
|
|
174
185
|
|
|
175
186
|
Templates are generated by `npx @dezkareid/osddt setup` and placed in each agent's commands directory. Each template corresponds to a step in the spec-driven workflow.
|
|
176
187
|
|
|
177
|
-
| Template
|
|
178
|
-
|
|
|
179
|
-
| `osddt.continue`
|
|
180
|
-
| `osddt.research`
|
|
181
|
-
| `osddt.start`
|
|
182
|
-
| `osddt.spec`
|
|
183
|
-
| `osddt.clarify`
|
|
184
|
-
| `osddt.plan`
|
|
185
|
-
| `osddt.tasks`
|
|
186
|
-
| `osddt.implement
|
|
187
|
-
| `osddt.fast`
|
|
188
|
-
| `osddt.done`
|
|
188
|
+
| Template | Description |
|
|
189
|
+
| ---------------- | ---------------------------------------------------------------------------------------- |
|
|
190
|
+
| `osddt.continue` | Detect the current workflow phase and prompt the next command |
|
|
191
|
+
| `osddt.research` | Research a topic and write a research file to inform the spec |
|
|
192
|
+
| `osddt.start` | Start a new feature — uses standard or worktree workflow based on `.osddtrc` |
|
|
193
|
+
| `osddt.spec` | Analyze requirements and write a feature specification |
|
|
194
|
+
| `osddt.clarify` | Resolve open questions in the spec and record decisions (optional) |
|
|
195
|
+
| `osddt.plan` | Create a technical implementation plan from a specification |
|
|
196
|
+
| `osddt.tasks` | Generate actionable tasks from an implementation plan |
|
|
197
|
+
| `osddt.implement`| Execute tasks from the task list one by one |
|
|
198
|
+
| `osddt.fast` | Bootstrap all planning artifacts (spec, plan, tasks) in one shot |
|
|
199
|
+
| `osddt.done` | Resolve project path, verify tasks, and move the feature to done |
|
|
189
200
|
|
|
190
201
|
#### Template Workflow
|
|
191
202
|
|
|
@@ -249,6 +260,33 @@ Templates are generated by `npx @dezkareid/osddt setup` and placed in each agent
|
|
|
249
260
|
/osddt.done
|
|
250
261
|
```
|
|
251
262
|
|
|
263
|
+
##### Example D — Parallel feature workflow (git worktree)
|
|
264
|
+
|
|
265
|
+
Enable worktree mode once during setup by providing the repository URL:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
npx @dezkareid/osddt setup --agents claude --repo-type single --worktree-repository https://github.com/org/repo.git
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
From that point on, `/osddt.start` automatically uses the worktree workflow:
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
/osddt.start add-payment-gateway
|
|
275
|
+
/osddt.spec
|
|
276
|
+
/osddt.plan use Stripe SDK, REST endpoints
|
|
277
|
+
/osddt.tasks
|
|
278
|
+
/osddt.implement
|
|
279
|
+
/osddt.done
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The worktree is created as a sibling of the repo root (e.g. `../my-repo-add-payment-gateway`). Active worktrees are tracked in `.osddt-worktrees` (sibling to the repo root) — a JSON array with entries containing `featureName`, `branch`, `worktreePath`, `workingDir`, and `repoRoot`. `osddt.continue` and `osddt.done` read this file to resolve the correct paths. When `osddt.done` runs, it archives the planning folder **and** removes the worktree automatically.
|
|
283
|
+
|
|
284
|
+
You can customise the worktree base directory via `worktreeBase` in `.osddtrc`:
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{ "repoType": "single", "worktree-repository": "https://github.com/org/repo.git", "worktreeBase": "/Users/me/worktrees" }
|
|
288
|
+
```
|
|
289
|
+
|
|
252
290
|
---
|
|
253
291
|
|
|
254
292
|
- Use **`osddt.research`** when you want to explore the codebase and gather findings before writing the spec. It creates the `working-on/<feature-name>/` folder and writes `osddt.research.md`.
|
|
@@ -276,13 +314,16 @@ Templates are generated by `npx @dezkareid/osddt setup` and placed in each agent
|
|
|
276
314
|
|
|
277
315
|
- **Input**: Either a human-readable feature description (e.g. `"Add user authentication"`) or an existing branch name (e.g. `feat/add-user-auth`).
|
|
278
316
|
- **Branch name resolution**: input is used as-is if it looks like a branch name; otherwise a name is derived (lowercased, hyphens, prefixed with `feat/`), subject to the 30-character feature name limit.
|
|
279
|
-
- **
|
|
317
|
+
- **Workflow mode**: determined by reading `.osddtrc`. If `"worktree-repository"` is present, the worktree workflow is used; otherwise the standard branch workflow is used. The user is never prompted to choose.
|
|
318
|
+
- **Standard workflow** (no `"worktree-repository"` in `.osddtrc`):
|
|
280
319
|
1. Checks for an existing branch — offers **Resume** (`git checkout`) or **Abort** if found, otherwise runs `git checkout -b <branch-name>`.
|
|
281
|
-
2.
|
|
282
|
-
3.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
320
|
+
2. Checks for an existing `working-on/<feature-name>/` folder — offers **Resume** or **Abort** if found, otherwise creates it.
|
|
321
|
+
3. Reports the branch and working directory.
|
|
322
|
+
- **Worktree workflow** (`"worktree-repository"` present in `.osddtrc`):
|
|
323
|
+
1. Derives the branch and feature name.
|
|
324
|
+
2. Runs `npx @dezkareid/osddt start-worktree <feature-name> [--dir <package-path>]`.
|
|
325
|
+
3. Navigates into the created worktree directory to locate the project root.
|
|
326
|
+
4. Reports the branch, worktree path, project root, and working directory.
|
|
286
327
|
|
|
287
328
|
#### osddt.research behaviour
|
|
288
329
|
|
|
@@ -311,7 +352,9 @@ Templates are generated by `npx @dezkareid/osddt setup` and placed in each agent
|
|
|
311
352
|
1. Reads `.osddtrc` to resolve the project path (single vs monorepo). For monorepos, asks the user which package.
|
|
312
353
|
2. Lists all folders under `working-on/`. If there is only one, uses it automatically; if there are multiple, asks the user to pick one.
|
|
313
354
|
3. Confirms all tasks in `osddt.tasks.md` are checked off (`- [x]`).
|
|
314
|
-
4. Runs `npx @dezkareid/osddt
|
|
355
|
+
4. Runs `npx @dezkareid/osddt worktree-info <feature-name>` to check if this is a worktree feature.
|
|
356
|
+
- If found: runs `npx @dezkareid/osddt done <feature-name> --dir <project-path> --worktree` (archives folder, removes worktree, cleans state file).
|
|
357
|
+
- If not found: runs `npx @dezkareid/osddt done <feature-name> --dir <project-path>` (existing behaviour).
|
|
315
358
|
|
|
316
359
|
Note: the `osddt done` CLI command does **not** read `.osddtrc` — project path resolution is the agent's responsibility, handled in step 1 above.
|
|
317
360
|
|
package/README.md
CHANGED
|
@@ -3,13 +3,18 @@ Other spec driven development tool but for monorepo
|
|
|
3
3
|
|
|
4
4
|
## CLI Commands
|
|
5
5
|
|
|
6
|
-
| Command
|
|
7
|
-
|
|
|
8
|
-
| `@dezkareid/osddt setup`
|
|
9
|
-
| `@dezkareid/osddt setup --agents <list> --repo-type <type>`
|
|
10
|
-
| `@dezkareid/osddt
|
|
11
|
-
| `@dezkareid/osddt
|
|
12
|
-
| `@dezkareid/osddt
|
|
6
|
+
| Command | Description |
|
|
7
|
+
| -------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
8
|
+
| `@dezkareid/osddt setup` | Generate agent command files for Claude and Gemini |
|
|
9
|
+
| `@dezkareid/osddt setup --agents <list> --repo-type <type>` | Non-interactive setup (for CI/scripted environments) |
|
|
10
|
+
| `@dezkareid/osddt setup --worktree-repository <url>` | Setup with worktree workflow enabled |
|
|
11
|
+
| `@dezkareid/osddt meta-info` | Output current branch and date as JSON |
|
|
12
|
+
| `@dezkareid/osddt done <feature-name> --dir <project-path>` | Move `working-on/<feature>` to `done/<feature>` |
|
|
13
|
+
| `@dezkareid/osddt done <feature-name> --dir <project-path> --worktree` | Archive feature, remove git worktree, and clean state file |
|
|
14
|
+
| `@dezkareid/osddt update` | Regenerate agent command files from the existing `.osddtrc` |
|
|
15
|
+
| `@dezkareid/osddt start-worktree <feature-name>` | Create a git worktree for a feature and scaffold working-on/ |
|
|
16
|
+
| `@dezkareid/osddt start-worktree <feature-name> --dir <package-path>` | Same, specifying the package path in a monorepo |
|
|
17
|
+
| `@dezkareid/osddt worktree-info <feature-name>` | Look up worktree paths for a feature (JSON output) |
|
|
13
18
|
|
|
14
19
|
### `osddt setup` options
|
|
15
20
|
|
|
@@ -17,11 +22,20 @@ Other spec driven development tool but for monorepo
|
|
|
17
22
|
| ---- | ------ | ----------- |
|
|
18
23
|
| `--agents <list>` | `claude`, `gemini` (comma-separated) | Skip the agents prompt and use the provided value(s) |
|
|
19
24
|
| `--repo-type <type>` | `single`, `monorepo` | Skip the repo type prompt and use the provided value |
|
|
25
|
+
| `--worktree-repository <url>` | any git URL | Enable worktree workflow and save the repository URL |
|
|
20
26
|
| `-d, --dir <directory>` | any path | Target directory (defaults to current working directory) |
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
All flags are optional. Providing neither runs the fully interactive mode. Providing `--agents` and `--repo-type` together skips all standard prompts. Adding `--worktree-repository` also runs environment checks and initialises the worktree state file.
|
|
23
29
|
|
|
24
|
-
The selected agents are saved in `.osddtrc`
|
|
30
|
+
The selected agents and config are saved in `.osddtrc` so that `osddt update` can regenerate the correct files without prompting.
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
// Standard mode
|
|
34
|
+
{ "repoType": "single", "agents": ["claude"] }
|
|
35
|
+
|
|
36
|
+
// Worktree mode — the "worktree-repository" field enables the worktree workflow
|
|
37
|
+
{ "repoType": "single", "agents": ["claude"], "worktree-repository": "https://github.com/org/repo.git" }
|
|
38
|
+
```
|
|
25
39
|
|
|
26
40
|
```bash
|
|
27
41
|
# Interactive (default)
|
|
@@ -106,6 +120,41 @@ flowchart LR
|
|
|
106
120
|
|
|
107
121
|
> Review the `## Assumptions` section of `osddt.plan.md` before implementing. Run `/osddt.clarify` if any Open Questions in the spec need resolving first.
|
|
108
122
|
|
|
123
|
+
#### Parallel feature workflow (git worktree)
|
|
124
|
+
|
|
125
|
+
Enable worktree mode by providing the repository URL during setup:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx @dezkareid/osddt setup --agents claude --repo-type single --worktree-repository https://github.com/org/repo.git
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This saves `"worktree-repository"` in `.osddtrc`, runs environment checks, and initialises the `.osddt-worktrees` state file. From that point on, `/osddt.start` automatically uses the worktree workflow:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
/osddt.start add-payment-gateway
|
|
135
|
+
/osddt.spec
|
|
136
|
+
/osddt.plan use Stripe SDK, REST endpoints
|
|
137
|
+
/osddt.tasks
|
|
138
|
+
/osddt.implement
|
|
139
|
+
/osddt.done
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The worktree is created as a sibling of your repo (e.g. `../my-repo-add-payment-gateway`). The `working-on/` planning folder lives inside the worktree alongside the code. When `osddt.done` runs, it archives the planning folder **and** removes the worktree automatically.
|
|
143
|
+
|
|
144
|
+
You can customise the worktree base directory by adding `worktreeBase` to `.osddtrc`:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"repoType": "single",
|
|
149
|
+
"worktree-repository": "https://github.com/org/repo.git",
|
|
150
|
+
"worktreeBase": "/Users/me/worktrees"
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Active worktrees are tracked in `.osddt-worktrees` (sibling to the repo root), a JSON array with entries containing `featureName`, `branch`, `worktreePath`, `workingDir`, and `repoRoot`. This file is the single source of truth used by `osddt.continue` and `osddt.done` to resolve the correct paths.
|
|
155
|
+
|
|
156
|
+
> **Migration note**: If you previously used `osddt setup-worktree` or `/osddt.start-worktree`, those commands have been removed. Re-run `osddt setup --worktree-repository <url>` to configure the new unified workflow. Manually delete any `osddt.start-worktree.*` files from your agent command directories — `osddt update` will not regenerate them.
|
|
157
|
+
|
|
109
158
|
#### Resuming after closing a session
|
|
110
159
|
|
|
111
160
|
```
|
|
@@ -118,18 +167,18 @@ flowchart LR
|
|
|
118
167
|
/osddt.done
|
|
119
168
|
```
|
|
120
169
|
|
|
121
|
-
| Template
|
|
122
|
-
|
|
|
123
|
-
| `osddt.continue`
|
|
124
|
-
| `osddt.research`
|
|
125
|
-
| `osddt.start`
|
|
126
|
-
| `osddt.spec`
|
|
127
|
-
| `osddt.clarify`
|
|
128
|
-
| `osddt.plan`
|
|
129
|
-
| `osddt.tasks`
|
|
130
|
-
| `osddt.implement
|
|
131
|
-
| `osddt.fast`
|
|
132
|
-
| `osddt.done`
|
|
170
|
+
| Template | Description |
|
|
171
|
+
| ---------------- | ------------------------------------------------------------------ |
|
|
172
|
+
| `osddt.continue` | Detect the current workflow phase and prompt the next command |
|
|
173
|
+
| `osddt.research` | Research a topic and write a research file to inform the spec |
|
|
174
|
+
| `osddt.start` | Start a new feature — uses standard or worktree workflow based on `.osddtrc` |
|
|
175
|
+
| `osddt.spec` | Analyze requirements and write a feature specification |
|
|
176
|
+
| `osddt.clarify` | Resolve open questions in the spec and record decisions (optional) |
|
|
177
|
+
| `osddt.plan` | Create a technical implementation plan from a specification |
|
|
178
|
+
| `osddt.tasks` | Generate actionable tasks from an implementation plan |
|
|
179
|
+
| `osddt.implement`| Execute tasks from the task list one by one |
|
|
180
|
+
| `osddt.fast` | Bootstrap all planning artifacts (spec, plan, tasks) in one shot |
|
|
181
|
+
| `osddt.done` | Resolve project path, verify tasks, and move the feature to done |
|
|
133
182
|
|
|
134
183
|
Generated files are placed in:
|
|
135
184
|
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,9 @@ import path from 'path';
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import select from '@inquirer/select';
|
|
7
7
|
import checkbox from '@inquirer/checkbox';
|
|
8
|
+
import input from '@inquirer/input';
|
|
8
9
|
import { execSync } from 'child_process';
|
|
10
|
+
import readline from 'readline';
|
|
9
11
|
|
|
10
12
|
function getRepoPreamble(npxCommand) {
|
|
11
13
|
return `## Context
|
|
@@ -18,15 +20,22 @@ ${npxCommand} meta-info
|
|
|
18
20
|
|
|
19
21
|
## Repository Configuration
|
|
20
22
|
|
|
21
|
-
Before proceeding, read the \`.osddtrc\` file in the root of the repository to determine the project path.
|
|
23
|
+
Before proceeding, read the \`.osddtrc\` file in the root of the repository to determine the project path and workflow mode.
|
|
22
24
|
|
|
23
25
|
\`\`\`json
|
|
24
|
-
//
|
|
25
|
-
{ "repoType": "monorepo" | "single" }
|
|
26
|
+
// standard mode
|
|
27
|
+
{ "repoType": "monorepo" | "single", "agents": ["claude"] }
|
|
28
|
+
|
|
29
|
+
// worktree mode — "worktree-repository" presence determines the workflow
|
|
30
|
+
{ "repoType": "monorepo" | "single", "agents": ["claude"], "worktree-repository": "https://github.com/org/repo.git" }
|
|
26
31
|
\`\`\`
|
|
27
32
|
|
|
28
33
|
- If \`repoType\` is \`"single"\`: the project path is the repository root.
|
|
29
34
|
- If \`repoType\` is \`"monorepo"\`: ask the user which package to work on (e.g. \`packages/my-package\`), then use \`<repo-root>/<package>\` as the project path.
|
|
35
|
+
- If \`"worktree-repository"\` is **present**: once the feature name is known, run \`${npxCommand} worktree-info <feature-name>\` to resolve the working directory:
|
|
36
|
+
- exit code **0**: parse the JSON and use the returned \`workingDir\` as the working directory.
|
|
37
|
+
- exit code **1**: the feature is not yet in a worktree — proceed as standard.
|
|
38
|
+
- If \`"worktree-repository"\` is **absent**: use the standard project path from \`.osddtrc\`.
|
|
30
39
|
|
|
31
40
|
## Working Directory
|
|
32
41
|
|
|
@@ -159,7 +168,9 @@ ${FEATURE_NAME_RULES}
|
|
|
159
168
|
|
|
160
169
|
Once the feature name is determined:
|
|
161
170
|
|
|
162
|
-
3.
|
|
171
|
+
3. Resolve the working directory:
|
|
172
|
+
- If \`"worktree-repository"\` is present in \`.osddtrc\`: run \`${npxCommand} worktree-info <feature-name>\` and use the returned \`workingDir\` if it exits with code 0; otherwise fall through to the standard path.
|
|
173
|
+
- Otherwise: ${WORKING_DIR_STEP}
|
|
163
174
|
|
|
164
175
|
4. Research the topic thoroughly:
|
|
165
176
|
- Explore the existing codebase for relevant patterns, conventions, and prior art
|
|
@@ -201,7 +212,37 @@ Apply the constraints below to the feature name (the segment after the last \`/\
|
|
|
201
212
|
|
|
202
213
|
${FEATURE_NAME_RULES}
|
|
203
214
|
|
|
204
|
-
Once the branch name is determined
|
|
215
|
+
Once the branch name is determined, choose the workflow based on \`.osddtrc\`:
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### If \`worktree-repository\` is **present** — Worktree workflow
|
|
220
|
+
|
|
221
|
+
3. Run the following command to create the git worktree, scaffold the working directory, and register the feature in the state file:
|
|
222
|
+
|
|
223
|
+
\`\`\`
|
|
224
|
+
${npxCommand} start-worktree <feature-name>
|
|
225
|
+
\`\`\`
|
|
226
|
+
|
|
227
|
+
For monorepos, pass the package path:
|
|
228
|
+
|
|
229
|
+
\`\`\`
|
|
230
|
+
${npxCommand} start-worktree <feature-name> --dir <package-path>
|
|
231
|
+
\`\`\`
|
|
232
|
+
|
|
233
|
+
4. Parse the command output to extract \`worktreePath\` and \`workingDir\`.
|
|
234
|
+
|
|
235
|
+
5. Navigate into the worktree directory to locate the project:
|
|
236
|
+
- Enter \`<worktreePath>\` — this is the isolated git worktree for this feature.
|
|
237
|
+
- If \`repoType\` is \`"single"\`: the project root is \`<worktreePath>\`.
|
|
238
|
+
- If \`repoType\` is \`"monorepo"\`: the project root is \`<worktreePath>/<package-path>\`.
|
|
239
|
+
- The planning files will live under \`<workingDir>\` (i.e. \`<project-root>/working-on/<feature-name>/\`).
|
|
240
|
+
|
|
241
|
+
6. Report the branch name, worktree path, project root, and working directory.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### If \`worktree-repository\` is **absent** — Standard workflow
|
|
205
246
|
|
|
206
247
|
3. Check whether the branch already exists locally or remotely:
|
|
207
248
|
- If it **does not exist**, create and switch to it:
|
|
@@ -218,6 +259,8 @@ Where \`<feature-name>\` is the last segment of the branch name (after the last
|
|
|
218
259
|
|
|
219
260
|
5. Report the branch name and working directory that were created or resumed.
|
|
220
261
|
|
|
262
|
+
---
|
|
263
|
+
|
|
221
264
|
${getCustomContextStep(npxCommand, 'start')}## Arguments
|
|
222
265
|
|
|
223
266
|
${args}
|
|
@@ -436,6 +479,26 @@ ${FEATURE_NAME_RULES}
|
|
|
436
479
|
|
|
437
480
|
### Step 2 — Create branch and working directory
|
|
438
481
|
|
|
482
|
+
Choose the workflow based on \`.osddtrc\`:
|
|
483
|
+
|
|
484
|
+
#### If \`worktree-repository\` is **present** — Worktree workflow
|
|
485
|
+
|
|
486
|
+
3. Run the following command to create the git worktree, scaffold the working directory, and register the feature in the state file:
|
|
487
|
+
|
|
488
|
+
\`\`\`
|
|
489
|
+
${npxCommand} start-worktree <feature-name>
|
|
490
|
+
\`\`\`
|
|
491
|
+
|
|
492
|
+
For monorepos, pass the package path:
|
|
493
|
+
|
|
494
|
+
\`\`\`
|
|
495
|
+
${npxCommand} start-worktree <feature-name> --dir <package-path>
|
|
496
|
+
\`\`\`
|
|
497
|
+
|
|
498
|
+
4. Parse the command output to extract \`worktreePath\` and \`workingDir\`. Navigate into \`<worktreePath>\` to locate the project root.
|
|
499
|
+
|
|
500
|
+
#### If \`worktree-repository\` is **absent** — Standard workflow
|
|
501
|
+
|
|
439
502
|
3. Check whether the branch already exists locally or remotely:
|
|
440
503
|
- If it **does not exist**, create and switch to it:
|
|
441
504
|
\`\`\`
|
|
@@ -502,16 +565,53 @@ ${args}
|
|
|
502
565
|
body: ({ npxCommand }) => `## Instructions
|
|
503
566
|
|
|
504
567
|
1. Confirm all tasks in \`osddt.tasks.md\` are checked off (\`- [x]\`)
|
|
505
|
-
2. Run the following command to
|
|
568
|
+
2. Run the following command to check whether this feature uses a git worktree:
|
|
506
569
|
|
|
507
570
|
\`\`\`
|
|
508
|
-
${npxCommand}
|
|
571
|
+
${npxCommand} worktree-info <feature-name>
|
|
509
572
|
\`\`\`
|
|
510
573
|
|
|
511
|
-
|
|
574
|
+
3. Based on the result:
|
|
575
|
+
|
|
576
|
+
**If it exits with code 1 (standard feature):** use the project path from \`.osddtrc\`, then run:
|
|
577
|
+
\`\`\`
|
|
578
|
+
${npxCommand} done <feature-name> --dir <project-path>
|
|
579
|
+
\`\`\`
|
|
580
|
+
Skip to step 8.
|
|
581
|
+
|
|
582
|
+
**If it exits with code 0 (worktree feature):** parse the JSON to get \`worktreePath\` and \`branch\`, derive \`<project-path>\` from \`workingDir\`, then continue below.
|
|
583
|
+
|
|
584
|
+
4. Check for uncommitted changes inside the worktree:
|
|
585
|
+
|
|
586
|
+
\`\`\`
|
|
587
|
+
git -C <worktreePath> status --porcelain
|
|
588
|
+
\`\`\`
|
|
589
|
+
|
|
590
|
+
5. If there are **uncommitted changes**:
|
|
591
|
+
1. Run \`git -C <worktreePath> diff\` to inspect them.
|
|
592
|
+
2. Derive a concise commit message in **conventional commit** format (e.g. \`feat: add payment gateway integration\`) based on the diff.
|
|
593
|
+
3. Present the proposed message to the user: _"Use this commit message, or provide your own?"_
|
|
594
|
+
4. Once confirmed, commit:
|
|
595
|
+
\`\`\`
|
|
596
|
+
git -C <worktreePath> add -A
|
|
597
|
+
git -C <worktreePath> commit -m "<confirmed-message>"
|
|
598
|
+
\`\`\`
|
|
599
|
+
|
|
600
|
+
6. Push the branch to remote (covers both first push and subsequent pushes):
|
|
601
|
+
|
|
602
|
+
\`\`\`
|
|
603
|
+
git -C <worktreePath> push --set-upstream origin <branch>
|
|
604
|
+
\`\`\`
|
|
605
|
+
|
|
606
|
+
7. Run the done command with the \`--worktree\` flag:
|
|
607
|
+
\`\`\`
|
|
608
|
+
${npxCommand} done <feature-name> --dir <project-path> --worktree
|
|
609
|
+
\`\`\`
|
|
610
|
+
|
|
611
|
+
8. The command will automatically prefix the destination folder name with today's date in \`YYYY-MM-DD\` format.
|
|
512
612
|
For example, \`working-on/feature-a\` will be moved to \`done/YYYY-MM-DD-feature-a\`.
|
|
513
613
|
|
|
514
|
-
|
|
614
|
+
9. Report the result of the command, including the full destination path
|
|
515
615
|
|
|
516
616
|
${getCustomContextStep(npxCommand, 'done')}`,
|
|
517
617
|
},
|
|
@@ -527,7 +627,7 @@ ${body}`;
|
|
|
527
627
|
}
|
|
528
628
|
function getClaudeTemplates(cwd, npxCommand) {
|
|
529
629
|
const dir = path.join(cwd, CLAUDE_COMMANDS_DIR);
|
|
530
|
-
return COMMAND_DEFINITIONS.map(
|
|
630
|
+
return COMMAND_DEFINITIONS.map(cmd => ({
|
|
531
631
|
filePath: path.join(dir, `${cmd.name}.md`),
|
|
532
632
|
content: formatClaudeCommand(cmd.description, cmd.body({ args: '$ARGUMENTS', npxCommand })),
|
|
533
633
|
}));
|
|
@@ -543,7 +643,7 @@ ${body}"""
|
|
|
543
643
|
}
|
|
544
644
|
function getGeminiTemplates(cwd, npxCommand) {
|
|
545
645
|
const dir = path.join(cwd, GEMINI_COMMANDS_DIR);
|
|
546
|
-
return COMMAND_DEFINITIONS.map(
|
|
646
|
+
return COMMAND_DEFINITIONS.map(cmd => ({
|
|
547
647
|
filePath: path.join(dir, `${cmd.name}.toml`),
|
|
548
648
|
content: formatGeminiCommand(cmd.description, cmd.body({ args: '{{args}}', npxCommand })),
|
|
549
649
|
}));
|
|
@@ -566,6 +666,12 @@ async function askRepoType() {
|
|
|
566
666
|
],
|
|
567
667
|
});
|
|
568
668
|
}
|
|
669
|
+
async function askWorktreeUrl() {
|
|
670
|
+
return input({
|
|
671
|
+
message: 'Remote repository URL for worktree workflow (leave blank to skip):',
|
|
672
|
+
default: '',
|
|
673
|
+
});
|
|
674
|
+
}
|
|
569
675
|
async function askAgents() {
|
|
570
676
|
return checkbox({
|
|
571
677
|
message: 'Which AI assistant tools do you want to set up?',
|
|
@@ -590,6 +696,102 @@ async function askAgents() {
|
|
|
590
696
|
});
|
|
591
697
|
}
|
|
592
698
|
|
|
699
|
+
function checkGitVersion() {
|
|
700
|
+
try {
|
|
701
|
+
const output = execSync('git --version', { encoding: 'utf-8' }).trim();
|
|
702
|
+
const match = output.match(/git version (\d+)\.(\d+)/);
|
|
703
|
+
if (!match) {
|
|
704
|
+
return { label: 'Git version >= 2.5', passed: false, detail: `Could not parse git version: ${output}` };
|
|
705
|
+
}
|
|
706
|
+
const major = parseInt(match[1], 10);
|
|
707
|
+
const minor = parseInt(match[2], 10);
|
|
708
|
+
const ok = major > 2 || (major === 2 && minor >= 5);
|
|
709
|
+
return {
|
|
710
|
+
label: 'Git version >= 2.5',
|
|
711
|
+
passed: ok,
|
|
712
|
+
detail: ok ? output : `Found ${output} — git worktree requires >= 2.5`,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
return { label: 'Git version >= 2.5', passed: false, detail: 'git not found or not executable' };
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function checkNotAWorktree(cwd) {
|
|
720
|
+
try {
|
|
721
|
+
const gitCommonDir = execSync('git rev-parse --git-common-dir', { cwd, encoding: 'utf-8' }).trim();
|
|
722
|
+
const gitDir = execSync('git rev-parse --git-dir', { cwd, encoding: 'utf-8' }).trim();
|
|
723
|
+
const isWorktree = gitDir !== gitCommonDir && gitDir !== '.git';
|
|
724
|
+
return {
|
|
725
|
+
label: 'Current directory is not a worktree',
|
|
726
|
+
passed: !isWorktree,
|
|
727
|
+
detail: isWorktree
|
|
728
|
+
? `This directory is itself a worktree (git-dir: ${gitDir}). Run setup from the main repository.`
|
|
729
|
+
: 'OK',
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
return { label: 'Current directory is not a worktree', passed: false, detail: 'Not inside a git repository' };
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async function checkTargetWritable(cwd) {
|
|
737
|
+
let targetBase;
|
|
738
|
+
try {
|
|
739
|
+
const repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
|
|
740
|
+
const rcPath = path.join(repoRoot, '.osddtrc');
|
|
741
|
+
if (await fs.pathExists(rcPath)) {
|
|
742
|
+
const rc = await fs.readJson(rcPath);
|
|
743
|
+
targetBase = rc.worktreeBase ?? path.dirname(repoRoot);
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
targetBase = path.dirname(repoRoot);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return { label: 'Worktree target directory is writable', passed: false, detail: 'Could not resolve repo root' };
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
await fs.access(targetBase, fs.constants.W_OK);
|
|
754
|
+
return { label: 'Worktree target directory is writable', passed: true, detail: `${targetBase} is writable` };
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return { label: 'Worktree target directory is writable', passed: false, detail: `${targetBase} is not writable` };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async function initStateFile(cwd) {
|
|
761
|
+
try {
|
|
762
|
+
const repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
|
|
763
|
+
const stateFile = path.join(path.dirname(repoRoot), '.osddt-worktrees');
|
|
764
|
+
if (!(await fs.pathExists(stateFile))) {
|
|
765
|
+
await fs.writeJson(stateFile, [], { spaces: 2 });
|
|
766
|
+
console.log(` ✓ Initialized worktree state file: ${stateFile}`);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
console.log(` ✓ Worktree state file already exists: ${stateFile}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
console.log(' ✗ Could not initialize worktree state file');
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function printCheckResult(result) {
|
|
777
|
+
const icon = result.passed ? '✓' : '✗';
|
|
778
|
+
console.log(` ${icon} ${result.label}`);
|
|
779
|
+
if (!result.passed) {
|
|
780
|
+
console.log(` → ${result.detail}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
async function runWorktreeChecks(cwd) {
|
|
784
|
+
const results = [
|
|
785
|
+
checkGitVersion(),
|
|
786
|
+
checkNotAWorktree(cwd),
|
|
787
|
+
await checkTargetWritable(cwd),
|
|
788
|
+
];
|
|
789
|
+
for (const result of results) {
|
|
790
|
+
printCheckResult(result);
|
|
791
|
+
}
|
|
792
|
+
return results.every(r => r.passed);
|
|
793
|
+
}
|
|
794
|
+
|
|
593
795
|
const CANONICAL_PACKAGE_NAME = '@dezkareid/osddt';
|
|
594
796
|
const NPX_COMMAND = 'npx osddt';
|
|
595
797
|
const NPX_COMMAND_FALLBACK = `npx ${CANONICAL_PACKAGE_NAME}`;
|
|
@@ -608,12 +810,12 @@ async function resolveNpxCommand(cwd) {
|
|
|
608
810
|
const VALID_AGENTS = ['claude', 'gemini'];
|
|
609
811
|
const VALID_REPO_TYPES = ['single', 'monorepo'];
|
|
610
812
|
function parseAgents(raw) {
|
|
611
|
-
const values = raw.split(',').map(
|
|
813
|
+
const values = raw.split(',').map(s => s.trim());
|
|
612
814
|
if (values.length === 0) {
|
|
613
815
|
console.error('Error: --agents requires at least one value.');
|
|
614
816
|
process.exit(1);
|
|
615
817
|
}
|
|
616
|
-
const invalid = values.filter(
|
|
818
|
+
const invalid = values.filter(v => !VALID_AGENTS.includes(v));
|
|
617
819
|
if (invalid.length > 0) {
|
|
618
820
|
console.error(`Error: Invalid agent(s): ${invalid.join(', ')}. Valid values: ${VALID_AGENTS.join(', ')}.`);
|
|
619
821
|
process.exit(1);
|
|
@@ -637,15 +839,7 @@ async function writeConfig(cwd, config) {
|
|
|
637
839
|
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
638
840
|
console.log(`\nSaved config: ${configPath}`);
|
|
639
841
|
}
|
|
640
|
-
async function
|
|
641
|
-
const agents = rawAgents !== undefined ? parseAgents(rawAgents) : await askAgents();
|
|
642
|
-
if (rawAgents === undefined)
|
|
643
|
-
console.log('');
|
|
644
|
-
const repoType = rawRepoType !== undefined ? parseRepoType(rawRepoType) : await askRepoType();
|
|
645
|
-
if (rawRepoType === undefined)
|
|
646
|
-
console.log('');
|
|
647
|
-
const npxCommand = await resolveNpxCommand(cwd);
|
|
648
|
-
console.log('Setting up OSDDT command files...\n');
|
|
842
|
+
async function writeAgentFiles(cwd, agents, npxCommand) {
|
|
649
843
|
if (agents.includes('claude')) {
|
|
650
844
|
const claudeFiles = getClaudeTemplates(cwd, npxCommand);
|
|
651
845
|
console.log('Claude Code commands (.claude/commands/):');
|
|
@@ -662,7 +856,38 @@ async function runSetup(cwd, rawAgents, rawRepoType) {
|
|
|
662
856
|
}
|
|
663
857
|
console.log('');
|
|
664
858
|
}
|
|
665
|
-
|
|
859
|
+
}
|
|
860
|
+
async function setupWorktreeEnvironment(cwd, _worktreeRepository) {
|
|
861
|
+
console.log('Checking environment for git worktree support...\n');
|
|
862
|
+
const allPassed = await runWorktreeChecks(cwd);
|
|
863
|
+
console.log('');
|
|
864
|
+
if (!allPassed) {
|
|
865
|
+
console.log('Some checks failed. Resolve the issues above before using the worktree workflow.');
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
await initStateFile(cwd);
|
|
869
|
+
console.log('');
|
|
870
|
+
}
|
|
871
|
+
async function runSetup(cwd, rawAgents, rawRepoType, rawWorktreeRepository) {
|
|
872
|
+
const agents = rawAgents !== undefined ? parseAgents(rawAgents) : await askAgents();
|
|
873
|
+
if (rawAgents === undefined)
|
|
874
|
+
console.log('');
|
|
875
|
+
const repoType = rawRepoType !== undefined ? parseRepoType(rawRepoType) : await askRepoType();
|
|
876
|
+
if (rawRepoType === undefined)
|
|
877
|
+
console.log('');
|
|
878
|
+
const worktreeRepository = rawWorktreeRepository !== undefined ? rawWorktreeRepository : (await askWorktreeUrl()) || undefined;
|
|
879
|
+
if (rawWorktreeRepository === undefined)
|
|
880
|
+
console.log('');
|
|
881
|
+
if (worktreeRepository) {
|
|
882
|
+
await setupWorktreeEnvironment(cwd);
|
|
883
|
+
}
|
|
884
|
+
const npxCommand = await resolveNpxCommand(cwd);
|
|
885
|
+
console.log('Setting up OSDDT command files...\n');
|
|
886
|
+
await writeAgentFiles(cwd, agents, npxCommand);
|
|
887
|
+
const config = { repoType, agents };
|
|
888
|
+
if (worktreeRepository)
|
|
889
|
+
config['worktree-repository'] = worktreeRepository;
|
|
890
|
+
await writeConfig(cwd, config);
|
|
666
891
|
console.log('\nSetup complete!');
|
|
667
892
|
console.log('Commands created: osddt.spec, osddt.plan, osddt.tasks, osddt.implement');
|
|
668
893
|
}
|
|
@@ -673,9 +898,10 @@ function setupCommand() {
|
|
|
673
898
|
.option('-d, --dir <directory>', 'target directory', process.cwd())
|
|
674
899
|
.option('--agents <list>', 'comma-separated agents to set up (claude, gemini)')
|
|
675
900
|
.option('--repo-type <type>', 'repository type (single, monorepo)')
|
|
901
|
+
.option('--worktree-repository <url>', 'remote repository URL to enable worktree workflow')
|
|
676
902
|
.action(async (options) => {
|
|
677
903
|
const targetDir = path.resolve(options.dir);
|
|
678
|
-
await runSetup(targetDir, options.agents, options.repoType);
|
|
904
|
+
await runSetup(targetDir, options.agents, options.repoType, options.worktreeRepository);
|
|
679
905
|
});
|
|
680
906
|
return cmd;
|
|
681
907
|
}
|
|
@@ -712,7 +938,13 @@ function todayPrefix() {
|
|
|
712
938
|
const dd = String(now.getDate()).padStart(2, '0');
|
|
713
939
|
return `${yyyy}-${mm}-${dd}`;
|
|
714
940
|
}
|
|
715
|
-
|
|
941
|
+
function resolveRepoRoot$2(cwd) {
|
|
942
|
+
return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
|
|
943
|
+
}
|
|
944
|
+
function stateFilePath$2(repoRoot) {
|
|
945
|
+
return path.join(path.dirname(repoRoot), '.osddt-worktrees');
|
|
946
|
+
}
|
|
947
|
+
async function runDone(featureName, cwd, worktree) {
|
|
716
948
|
const src = path.join(cwd, 'working-on', featureName);
|
|
717
949
|
const destName = `${todayPrefix()}-${featureName}`;
|
|
718
950
|
const dest = path.join(cwd, 'done', destName);
|
|
@@ -723,6 +955,30 @@ async function runDone(featureName, cwd) {
|
|
|
723
955
|
await fs.ensureDir(path.dirname(dest));
|
|
724
956
|
await fs.move(src, dest);
|
|
725
957
|
console.log(`Moved: working-on/${featureName} → done/${destName}`);
|
|
958
|
+
if (!worktree)
|
|
959
|
+
return;
|
|
960
|
+
const repoRoot = resolveRepoRoot$2(process.cwd());
|
|
961
|
+
const stateFile = stateFilePath$2(repoRoot);
|
|
962
|
+
if (!(await fs.pathExists(stateFile))) {
|
|
963
|
+
console.error(`Warning: .osddt-worktrees not found at ${stateFile}. Skipping worktree cleanup.`);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const entries = await fs.readJson(stateFile);
|
|
967
|
+
const entry = entries.find(e => e.featureName === featureName);
|
|
968
|
+
if (!entry) {
|
|
969
|
+
console.error(`Warning: No worktree entry found for "${featureName}". Skipping worktree cleanup.`);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (await fs.pathExists(entry.worktreePath)) {
|
|
973
|
+
execSync(`git worktree remove "${entry.worktreePath}" --force`, { cwd: repoRoot, stdio: 'inherit' });
|
|
974
|
+
console.log(`Removed worktree: ${entry.worktreePath}`);
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
console.log(`Worktree path not found on filesystem, skipping git worktree remove.`);
|
|
978
|
+
}
|
|
979
|
+
const updated = entries.filter(e => e.featureName !== featureName);
|
|
980
|
+
await fs.writeJson(stateFile, updated, { spaces: 2 });
|
|
981
|
+
console.log(`Removed state entry for "${featureName}" from .osddt-worktrees`);
|
|
726
982
|
}
|
|
727
983
|
function doneCommand() {
|
|
728
984
|
const cmd = new Command('done');
|
|
@@ -730,9 +986,10 @@ function doneCommand() {
|
|
|
730
986
|
.description('Move a feature from working-on/<feature-name> to done/<feature-name>')
|
|
731
987
|
.argument('<feature-name>', 'name of the feature to mark as done')
|
|
732
988
|
.option('-d, --dir <directory>', 'project directory', process.cwd())
|
|
989
|
+
.option('--worktree', 'also remove the git worktree and clean up the state file')
|
|
733
990
|
.action(async (featureName, options) => {
|
|
734
991
|
const targetDir = path.resolve(options.dir);
|
|
735
|
-
await runDone(featureName, targetDir);
|
|
992
|
+
await runDone(featureName, targetDir, options.worktree ?? false);
|
|
736
993
|
});
|
|
737
994
|
return cmd;
|
|
738
995
|
}
|
|
@@ -745,7 +1002,7 @@ async function hasOsddtCommandFile(dir, pattern) {
|
|
|
745
1002
|
if (!(await fs.pathExists(dir)))
|
|
746
1003
|
return false;
|
|
747
1004
|
const entries = await fs.readdir(dir);
|
|
748
|
-
return entries.some(
|
|
1005
|
+
return entries.some(f => pattern.test(f));
|
|
749
1006
|
}
|
|
750
1007
|
async function inferAgents(cwd) {
|
|
751
1008
|
const detected = [];
|
|
@@ -868,6 +1125,169 @@ function contextCommand() {
|
|
|
868
1125
|
return cmd;
|
|
869
1126
|
}
|
|
870
1127
|
|
|
1128
|
+
function resolveRepoRoot$1(cwd) {
|
|
1129
|
+
return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
|
|
1130
|
+
}
|
|
1131
|
+
function repoName(repoRoot) {
|
|
1132
|
+
return path.basename(repoRoot);
|
|
1133
|
+
}
|
|
1134
|
+
function stateFilePath$1(repoRoot) {
|
|
1135
|
+
return path.join(path.dirname(repoRoot), '.osddt-worktrees');
|
|
1136
|
+
}
|
|
1137
|
+
async function readStateFile(stateFile) {
|
|
1138
|
+
if (!(await fs.pathExists(stateFile)))
|
|
1139
|
+
return [];
|
|
1140
|
+
return fs.readJson(stateFile);
|
|
1141
|
+
}
|
|
1142
|
+
async function writeStateFile(stateFile, entries) {
|
|
1143
|
+
await fs.writeJson(stateFile, entries, { spaces: 2 });
|
|
1144
|
+
}
|
|
1145
|
+
function branchExists(branch, cwd) {
|
|
1146
|
+
try {
|
|
1147
|
+
execSync(`git rev-parse --verify ${branch}`, { cwd, stdio: 'ignore' });
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
catch {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function remoteBranchExists(branch, cwd) {
|
|
1155
|
+
try {
|
|
1156
|
+
execSync(`git ls-remote --exit-code --heads origin ${branch}`, { cwd, stdio: 'ignore' });
|
|
1157
|
+
return true;
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
return false;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
async function prompt(question) {
|
|
1164
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1165
|
+
return new Promise((resolve) => {
|
|
1166
|
+
rl.question(question, (answer) => {
|
|
1167
|
+
rl.close();
|
|
1168
|
+
resolve(answer.trim());
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
async function createWorktree(branch, worktreePath, repoRoot) {
|
|
1173
|
+
if (await fs.pathExists(worktreePath)) {
|
|
1174
|
+
console.log(`\nDirectory already exists at: ${worktreePath}`);
|
|
1175
|
+
const answer = await prompt('Resume or Abort? [R/a] ');
|
|
1176
|
+
if (answer.toLowerCase() === 'a') {
|
|
1177
|
+
console.log('Aborted.');
|
|
1178
|
+
process.exit(0);
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const localExists = branchExists(branch, repoRoot);
|
|
1183
|
+
const remoteExists = !localExists && remoteBranchExists(branch, repoRoot);
|
|
1184
|
+
if (localExists || remoteExists) {
|
|
1185
|
+
console.log(`\nBranch "${branch}" already exists ${localExists ? 'locally' : 'on remote'}.`);
|
|
1186
|
+
const answer = await prompt('Resume or Abort? [R/a] ');
|
|
1187
|
+
if (answer.toLowerCase() === 'a') {
|
|
1188
|
+
console.log('Aborted.');
|
|
1189
|
+
process.exit(0);
|
|
1190
|
+
}
|
|
1191
|
+
execSync(`git worktree add "${worktreePath}" ${branch}`, { cwd: repoRoot, stdio: 'inherit' });
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
execSync(`git worktree add "${worktreePath}" -b ${branch}`, { cwd: repoRoot, stdio: 'inherit' });
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
async function runStartWorktree(featureName, options) {
|
|
1198
|
+
const cwd = process.cwd();
|
|
1199
|
+
const repoRoot = resolveRepoRoot$1(cwd);
|
|
1200
|
+
const branch = `feat/${featureName}`;
|
|
1201
|
+
// Read .osddtrc
|
|
1202
|
+
const rcPath = path.join(repoRoot, '.osddtrc');
|
|
1203
|
+
let rc = { repoType: 'single' };
|
|
1204
|
+
if (await fs.pathExists(rcPath)) {
|
|
1205
|
+
rc = await fs.readJson(rcPath);
|
|
1206
|
+
}
|
|
1207
|
+
// Resolve worktree path
|
|
1208
|
+
const base = rc.worktreeBase ?? path.dirname(repoRoot);
|
|
1209
|
+
const worktreePath = path.join(base, `${repoName(repoRoot)}-${featureName}`);
|
|
1210
|
+
// Check state file for existing entry
|
|
1211
|
+
const stateFile = stateFilePath$1(repoRoot);
|
|
1212
|
+
const entries = await readStateFile(stateFile);
|
|
1213
|
+
const existing = entries.find(e => e.featureName === featureName);
|
|
1214
|
+
if (existing) {
|
|
1215
|
+
console.log(`\nWorktree for "${featureName}" already exists at: ${existing.worktreePath}`);
|
|
1216
|
+
const answer = await prompt('Resume or Abort? [R/a] ');
|
|
1217
|
+
if (answer.toLowerCase() === 'a') {
|
|
1218
|
+
console.log('Aborted.');
|
|
1219
|
+
process.exit(0);
|
|
1220
|
+
}
|
|
1221
|
+
console.log(`\nResuming existing worktree.`);
|
|
1222
|
+
console.log(` Branch: ${existing.branch}`);
|
|
1223
|
+
console.log(` Worktree path: ${existing.worktreePath}`);
|
|
1224
|
+
console.log(` Working dir: ${existing.workingDir}`);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
await createWorktree(branch, worktreePath, repoRoot);
|
|
1228
|
+
// Resolve working dir
|
|
1229
|
+
let projectPath;
|
|
1230
|
+
if (rc.repoType === 'monorepo') {
|
|
1231
|
+
const pkg = options.dir ?? await prompt('Package path (e.g. packages/my-package): ');
|
|
1232
|
+
projectPath = path.join(worktreePath, pkg);
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
projectPath = worktreePath;
|
|
1236
|
+
}
|
|
1237
|
+
const workingDir = path.join(projectPath, 'working-on', featureName);
|
|
1238
|
+
await fs.ensureDir(workingDir);
|
|
1239
|
+
// Write state file entry
|
|
1240
|
+
entries.push({ featureName, branch, worktreePath, workingDir, repoRoot });
|
|
1241
|
+
await writeStateFile(stateFile, entries);
|
|
1242
|
+
console.log(`\nWorktree feature started:`);
|
|
1243
|
+
console.log(` Branch: ${branch}`);
|
|
1244
|
+
console.log(` Worktree path: ${worktreePath}`);
|
|
1245
|
+
console.log(` Working dir: ${workingDir}`);
|
|
1246
|
+
}
|
|
1247
|
+
function startWorktreeCommand() {
|
|
1248
|
+
const cmd = new Command('start-worktree');
|
|
1249
|
+
cmd
|
|
1250
|
+
.description('Start a new feature using a git worktree')
|
|
1251
|
+
.argument('<feature-name>', 'feature name (kebab-case, max 30 chars)')
|
|
1252
|
+
.option('-d, --dir <package>', 'package path within monorepo (skips prompt)')
|
|
1253
|
+
.action(async (featureName, options) => {
|
|
1254
|
+
await runStartWorktree(featureName, options);
|
|
1255
|
+
});
|
|
1256
|
+
return cmd;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function resolveRepoRoot(cwd) {
|
|
1260
|
+
return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
|
|
1261
|
+
}
|
|
1262
|
+
function stateFilePath(repoRoot) {
|
|
1263
|
+
return path.join(path.dirname(repoRoot), '.osddt-worktrees');
|
|
1264
|
+
}
|
|
1265
|
+
async function runWorktreeInfo(featureName) {
|
|
1266
|
+
const repoRoot = resolveRepoRoot(process.cwd());
|
|
1267
|
+
const stateFile = stateFilePath(repoRoot);
|
|
1268
|
+
if (!(await fs.pathExists(stateFile))) {
|
|
1269
|
+
console.error(`No .osddt-worktrees file found at: ${stateFile}`);
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
const entries = await fs.readJson(stateFile);
|
|
1273
|
+
const entry = entries.find(e => e.featureName === featureName);
|
|
1274
|
+
if (!entry) {
|
|
1275
|
+
console.error(`No worktree entry found for feature: ${featureName}`);
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1278
|
+
console.log(JSON.stringify({ worktreePath: entry.worktreePath, workingDir: entry.workingDir, branch: entry.branch }));
|
|
1279
|
+
}
|
|
1280
|
+
function worktreeInfoCommand() {
|
|
1281
|
+
const cmd = new Command('worktree-info');
|
|
1282
|
+
cmd
|
|
1283
|
+
.description('Look up worktree paths for a feature from the state file')
|
|
1284
|
+
.argument('<feature-name>', 'feature name to look up')
|
|
1285
|
+
.action(async (featureName) => {
|
|
1286
|
+
await runWorktreeInfo(featureName);
|
|
1287
|
+
});
|
|
1288
|
+
return cmd;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
871
1291
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
872
1292
|
const __dirname$1 = path.dirname(__filename$1);
|
|
873
1293
|
const pkgPath = path.join(__dirname$1, '..', 'package.json');
|
|
@@ -882,4 +1302,6 @@ program.addCommand(metaInfoCommand());
|
|
|
882
1302
|
program.addCommand(doneCommand());
|
|
883
1303
|
program.addCommand(updateCommand());
|
|
884
1304
|
program.addCommand(contextCommand());
|
|
1305
|
+
program.addCommand(startWorktreeCommand());
|
|
1306
|
+
program.addCommand(worktreeInfoCommand());
|
|
885
1307
|
program.parse(process.argv);
|
package/dist/utils/prompt.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type RepoType = 'monorepo' | 'single';
|
|
2
2
|
export type AgentType = 'claude' | 'gemini';
|
|
3
3
|
export declare function askRepoType(): Promise<RepoType>;
|
|
4
|
+
export declare function askWorktreeUrl(): Promise<string>;
|
|
4
5
|
export declare function askAgents(): Promise<AgentType[]>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface CheckResult {
|
|
2
|
+
label: string;
|
|
3
|
+
passed: boolean;
|
|
4
|
+
detail: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function checkGitVersion(): CheckResult;
|
|
7
|
+
export declare function checkNotAWorktree(cwd: string): CheckResult;
|
|
8
|
+
export declare function checkTargetWritable(cwd: string): Promise<CheckResult>;
|
|
9
|
+
export declare function initStateFile(cwd: string): Promise<void>;
|
|
10
|
+
export declare function printCheckResult(result: CheckResult): void;
|
|
11
|
+
export declare function runWorktreeChecks(cwd: string): Promise<boolean>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dezkareid/osddt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "Package for Spec-Driven Development workflow",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spec-driven",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"main": "./dist/index.js",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@inquirer/checkbox": "5.0.7",
|
|
37
|
+
"@inquirer/input": "5.0.7",
|
|
37
38
|
"@inquirer/select": "5.0.7",
|
|
38
39
|
"commander": "12.0.0",
|
|
39
40
|
"fs-extra": "11.2.0",
|
|
@@ -42,13 +43,12 @@
|
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@commitlint/cli": "20.5.0",
|
|
44
45
|
"@commitlint/config-conventional": "20.5.0",
|
|
45
|
-
"@dezkareid/eslint-
|
|
46
|
+
"@dezkareid/eslint-plugin-web": "1.0.0",
|
|
46
47
|
"@rollup/plugin-typescript": "12.1.4",
|
|
47
48
|
"@types/fs-extra": "11.0.4",
|
|
48
49
|
"@types/node": "25.0.10",
|
|
49
50
|
"eslint": "9.39.2",
|
|
50
51
|
"husky": "9.1.7",
|
|
51
|
-
"prettier": "3.8.1",
|
|
52
52
|
"rollup": "4.56.0",
|
|
53
53
|
"typescript": "5.9.3",
|
|
54
54
|
"vitest": "4.0.18"
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"test": "vitest run",
|
|
60
60
|
"test:watch": "vitest",
|
|
61
61
|
"lint": "eslint src",
|
|
62
|
-
"
|
|
62
|
+
"lint:fix": "eslint src --fix",
|
|
63
63
|
"setup-local": "pnpm run build && npx osddt setup --agents claude --repo-type single",
|
|
64
64
|
"update-local": "pnpm run build && npx osddt update"
|
|
65
65
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|