@saga-ai/cli 0.2.0 → 0.4.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/README.md +18 -0
- package/dist/cli.cjs +140 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Run commands using npx (no global installation required):
|
|
|
14
14
|
```bash
|
|
15
15
|
npx @saga-ai/cli init
|
|
16
16
|
npx @saga-ai/cli find <query>
|
|
17
|
+
npx @saga-ai/cli worktree <epic-slug> <story-slug>
|
|
17
18
|
npx @saga-ai/cli implement <story-slug>
|
|
18
19
|
npx @saga-ai/cli dashboard
|
|
19
20
|
```
|
|
@@ -56,6 +57,23 @@ Returns JSON with:
|
|
|
56
57
|
|
|
57
58
|
Supports typo tolerance (e.g., "implment" matches "implement").
|
|
58
59
|
|
|
60
|
+
### `saga worktree <epic-slug> <story-slug>`
|
|
61
|
+
|
|
62
|
+
Create a git worktree and branch for story isolation.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
saga worktree my-epic my-story
|
|
66
|
+
saga worktree auth-system login-form --path /path/to/project
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Creates:
|
|
70
|
+
- Branch: `story-<story-slug>-epic-<epic-slug>`
|
|
71
|
+
- Worktree: `.saga/worktrees/<epic-slug>/<story-slug>/`
|
|
72
|
+
|
|
73
|
+
Returns JSON with:
|
|
74
|
+
- `success: true` + `worktreePath`, `branch` - Worktree created
|
|
75
|
+
- `success: false` + `error` - Creation failed (branch exists, directory exists, etc.)
|
|
76
|
+
|
|
59
77
|
### `saga implement <story-slug>`
|
|
60
78
|
|
|
61
79
|
Run autonomous story implementation using Claude workers.
|
package/dist/cli.cjs
CHANGED
|
@@ -3037,8 +3037,8 @@ var {
|
|
|
3037
3037
|
} = import_index.default;
|
|
3038
3038
|
|
|
3039
3039
|
// src/cli.ts
|
|
3040
|
-
var
|
|
3041
|
-
var
|
|
3040
|
+
var import_node_path7 = require("node:path");
|
|
3041
|
+
var import_node_fs6 = require("node:fs");
|
|
3042
3042
|
|
|
3043
3043
|
// src/commands/init.ts
|
|
3044
3044
|
var import_node_path2 = require("node:path");
|
|
@@ -4957,7 +4957,7 @@ function buildScopeSettings() {
|
|
|
4957
4957
|
hooks: {
|
|
4958
4958
|
PreToolUse: [
|
|
4959
4959
|
{
|
|
4960
|
-
matcher: "Read|Write|Edit",
|
|
4960
|
+
matcher: "Read|Write|Edit|Glob|Grep",
|
|
4961
4961
|
hooks: [hookCommand]
|
|
4962
4962
|
}
|
|
4963
4963
|
]
|
|
@@ -5196,10 +5196,12 @@ async function dashboardCommand(options) {
|
|
|
5196
5196
|
}
|
|
5197
5197
|
|
|
5198
5198
|
// src/commands/scope-validator.ts
|
|
5199
|
-
|
|
5199
|
+
var import_node_path5 = require("node:path");
|
|
5200
|
+
function getFilePathFromInput(hookInput) {
|
|
5200
5201
|
try {
|
|
5201
|
-
const data = JSON.parse(
|
|
5202
|
-
|
|
5202
|
+
const data = JSON.parse(hookInput);
|
|
5203
|
+
const toolInput = data.tool_input || {};
|
|
5204
|
+
return toolInput.file_path || toolInput.path || null;
|
|
5203
5205
|
} catch {
|
|
5204
5206
|
return null;
|
|
5205
5207
|
}
|
|
@@ -5213,6 +5215,15 @@ function normalizePath(path) {
|
|
|
5213
5215
|
function isArchiveAccess(path) {
|
|
5214
5216
|
return path.includes(".saga/archive");
|
|
5215
5217
|
}
|
|
5218
|
+
function isWithinWorktree(filePath, worktreePath) {
|
|
5219
|
+
const absoluteFilePath = (0, import_node_path5.resolve)(filePath);
|
|
5220
|
+
const absoluteWorktree = (0, import_node_path5.resolve)(worktreePath);
|
|
5221
|
+
const relativePath = (0, import_node_path5.relative)(absoluteWorktree, absoluteFilePath);
|
|
5222
|
+
if (relativePath.startsWith("..") || (0, import_node_path5.resolve)(relativePath) === relativePath) {
|
|
5223
|
+
return false;
|
|
5224
|
+
}
|
|
5225
|
+
return true;
|
|
5226
|
+
}
|
|
5216
5227
|
function checkStoryAccess(path, allowedEpic, allowedStory) {
|
|
5217
5228
|
if (!path.includes(".saga/epics/")) {
|
|
5218
5229
|
return true;
|
|
@@ -5233,24 +5244,27 @@ function checkStoryAccess(path, allowedEpic, allowedStory) {
|
|
|
5233
5244
|
return pathEpic === allowedEpic;
|
|
5234
5245
|
}
|
|
5235
5246
|
}
|
|
5236
|
-
function printScopeViolation(filePath, epicSlug, storySlug, reason) {
|
|
5247
|
+
function printScopeViolation(filePath, epicSlug, storySlug, worktreePath, reason) {
|
|
5237
5248
|
console.error(`SCOPE VIOLATION: ${reason}
|
|
5238
5249
|
|
|
5239
5250
|
Attempted path: ${filePath}
|
|
5240
5251
|
|
|
5241
5252
|
Your scope is limited to:
|
|
5253
|
+
Worktree: ${worktreePath}
|
|
5242
5254
|
Epic: ${epicSlug}
|
|
5243
5255
|
Story: ${storySlug}
|
|
5244
|
-
Allowed: .saga/epics/${epicSlug}/stories/${storySlug}/
|
|
5256
|
+
Allowed story files: .saga/epics/${epicSlug}/stories/${storySlug}/
|
|
5245
5257
|
|
|
5258
|
+
Workers cannot access files outside the worktree directory.
|
|
5246
5259
|
To access other stories, start a new /implement session for that story.`);
|
|
5247
5260
|
}
|
|
5248
5261
|
async function scopeValidatorCommand() {
|
|
5262
|
+
const worktreePath = process.env.SAGA_PROJECT_DIR || "";
|
|
5249
5263
|
const epicSlug = process.env.SAGA_EPIC_SLUG || "";
|
|
5250
5264
|
const storySlug = process.env.SAGA_STORY_SLUG || "";
|
|
5251
|
-
if (!epicSlug || !storySlug) {
|
|
5265
|
+
if (!worktreePath || !epicSlug || !storySlug) {
|
|
5252
5266
|
console.error(
|
|
5253
|
-
"ERROR: scope-validator requires SAGA_EPIC_SLUG and SAGA_STORY_SLUG environment variables"
|
|
5267
|
+
"ERROR: scope-validator requires SAGA_PROJECT_DIR, SAGA_EPIC_SLUG, and SAGA_STORY_SLUG environment variables"
|
|
5254
5268
|
);
|
|
5255
5269
|
process.exit(2);
|
|
5256
5270
|
}
|
|
@@ -5264,11 +5278,22 @@ async function scopeValidatorCommand() {
|
|
|
5264
5278
|
process.exit(0);
|
|
5265
5279
|
}
|
|
5266
5280
|
const normPath = normalizePath(filePath);
|
|
5281
|
+
if (!isWithinWorktree(normPath, worktreePath)) {
|
|
5282
|
+
printScopeViolation(
|
|
5283
|
+
filePath,
|
|
5284
|
+
epicSlug,
|
|
5285
|
+
storySlug,
|
|
5286
|
+
worktreePath,
|
|
5287
|
+
"Access outside worktree blocked\nReason: Workers can only access files within their assigned worktree directory."
|
|
5288
|
+
);
|
|
5289
|
+
process.exit(2);
|
|
5290
|
+
}
|
|
5267
5291
|
if (isArchiveAccess(normPath)) {
|
|
5268
5292
|
printScopeViolation(
|
|
5269
5293
|
filePath,
|
|
5270
5294
|
epicSlug,
|
|
5271
5295
|
storySlug,
|
|
5296
|
+
worktreePath,
|
|
5272
5297
|
"Access to archive folder blocked\nReason: The archive folder contains completed stories and is read-only during execution."
|
|
5273
5298
|
);
|
|
5274
5299
|
process.exit(2);
|
|
@@ -5278,6 +5303,7 @@ async function scopeValidatorCommand() {
|
|
|
5278
5303
|
filePath,
|
|
5279
5304
|
epicSlug,
|
|
5280
5305
|
storySlug,
|
|
5306
|
+
worktreePath,
|
|
5281
5307
|
"Access to other story blocked\nReason: Workers can only access their assigned story's files."
|
|
5282
5308
|
);
|
|
5283
5309
|
process.exit(2);
|
|
@@ -5307,9 +5333,107 @@ async function findCommand(query, options) {
|
|
|
5307
5333
|
}
|
|
5308
5334
|
}
|
|
5309
5335
|
|
|
5336
|
+
// src/commands/worktree.ts
|
|
5337
|
+
var import_node_path6 = require("node:path");
|
|
5338
|
+
var import_node_fs5 = require("node:fs");
|
|
5339
|
+
var import_node_child_process2 = require("node:child_process");
|
|
5340
|
+
function runGitCommand(args, cwd) {
|
|
5341
|
+
try {
|
|
5342
|
+
const output = (0, import_node_child_process2.execSync)(`git ${args.join(" ")}`, {
|
|
5343
|
+
cwd,
|
|
5344
|
+
encoding: "utf-8",
|
|
5345
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5346
|
+
});
|
|
5347
|
+
return { success: true, output: output.trim() };
|
|
5348
|
+
} catch (error) {
|
|
5349
|
+
const stderr = error.stderr?.toString().trim() || error.message;
|
|
5350
|
+
return { success: false, output: stderr };
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
function branchExists(branchName, cwd) {
|
|
5354
|
+
const result = runGitCommand(["rev-parse", "--verify", branchName], cwd);
|
|
5355
|
+
return result.success;
|
|
5356
|
+
}
|
|
5357
|
+
function getMainBranch(cwd) {
|
|
5358
|
+
const result = runGitCommand(
|
|
5359
|
+
["symbolic-ref", "refs/remotes/origin/HEAD"],
|
|
5360
|
+
cwd
|
|
5361
|
+
);
|
|
5362
|
+
if (result.success) {
|
|
5363
|
+
return result.output.replace("refs/remotes/origin/", "");
|
|
5364
|
+
}
|
|
5365
|
+
return "main";
|
|
5366
|
+
}
|
|
5367
|
+
function createWorktree(projectPath, epicSlug, storySlug) {
|
|
5368
|
+
const branchName = `story-${storySlug}-epic-${epicSlug}`;
|
|
5369
|
+
const worktreePath = (0, import_node_path6.join)(
|
|
5370
|
+
projectPath,
|
|
5371
|
+
".saga",
|
|
5372
|
+
"worktrees",
|
|
5373
|
+
epicSlug,
|
|
5374
|
+
storySlug
|
|
5375
|
+
);
|
|
5376
|
+
if (branchExists(branchName, projectPath)) {
|
|
5377
|
+
return {
|
|
5378
|
+
success: false,
|
|
5379
|
+
error: `Branch already exists: ${branchName}`
|
|
5380
|
+
};
|
|
5381
|
+
}
|
|
5382
|
+
if ((0, import_node_fs5.existsSync)(worktreePath)) {
|
|
5383
|
+
return {
|
|
5384
|
+
success: false,
|
|
5385
|
+
error: `Worktree directory already exists: ${worktreePath}`
|
|
5386
|
+
};
|
|
5387
|
+
}
|
|
5388
|
+
const mainBranch = getMainBranch(projectPath);
|
|
5389
|
+
runGitCommand(["fetch", "origin", mainBranch], projectPath);
|
|
5390
|
+
const createBranchResult = runGitCommand(
|
|
5391
|
+
["branch", branchName, `origin/${mainBranch}`],
|
|
5392
|
+
projectPath
|
|
5393
|
+
);
|
|
5394
|
+
if (!createBranchResult.success) {
|
|
5395
|
+
return {
|
|
5396
|
+
success: false,
|
|
5397
|
+
error: `Failed to create branch: ${createBranchResult.output}`
|
|
5398
|
+
};
|
|
5399
|
+
}
|
|
5400
|
+
const worktreeParent = (0, import_node_path6.join)(projectPath, ".saga", "worktrees", epicSlug);
|
|
5401
|
+
(0, import_node_fs5.mkdirSync)(worktreeParent, { recursive: true });
|
|
5402
|
+
const createWorktreeResult = runGitCommand(
|
|
5403
|
+
["worktree", "add", worktreePath, branchName],
|
|
5404
|
+
projectPath
|
|
5405
|
+
);
|
|
5406
|
+
if (!createWorktreeResult.success) {
|
|
5407
|
+
return {
|
|
5408
|
+
success: false,
|
|
5409
|
+
error: `Failed to create worktree: ${createWorktreeResult.output}`
|
|
5410
|
+
};
|
|
5411
|
+
}
|
|
5412
|
+
return {
|
|
5413
|
+
success: true,
|
|
5414
|
+
worktreePath,
|
|
5415
|
+
branch: branchName
|
|
5416
|
+
};
|
|
5417
|
+
}
|
|
5418
|
+
async function worktreeCommand(epicSlug, storySlug, options) {
|
|
5419
|
+
let projectPath;
|
|
5420
|
+
try {
|
|
5421
|
+
projectPath = resolveProjectPath(options.path);
|
|
5422
|
+
} catch (error) {
|
|
5423
|
+
const result2 = { success: false, error: error.message };
|
|
5424
|
+
console.log(JSON.stringify(result2));
|
|
5425
|
+
process.exit(1);
|
|
5426
|
+
}
|
|
5427
|
+
const result = createWorktree(projectPath, epicSlug, storySlug);
|
|
5428
|
+
console.log(JSON.stringify(result));
|
|
5429
|
+
if (!result.success) {
|
|
5430
|
+
process.exit(1);
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
|
|
5310
5434
|
// src/cli.ts
|
|
5311
|
-
var packageJsonPath = (0,
|
|
5312
|
-
var packageJson = JSON.parse((0,
|
|
5435
|
+
var packageJsonPath = (0, import_node_path7.join)(__dirname, "..", "package.json");
|
|
5436
|
+
var packageJson = JSON.parse((0, import_node_fs6.readFileSync)(packageJsonPath, "utf-8"));
|
|
5313
5437
|
var program2 = new Command();
|
|
5314
5438
|
program2.name("saga").description("CLI for SAGA - Structured Autonomous Goal Achievement").version(packageJson.version).addHelpCommand("help [command]", "Display help for a command");
|
|
5315
5439
|
program2.option("-p, --path <dir>", "Path to SAGA project directory (overrides auto-discovery)");
|
|
@@ -5334,6 +5458,10 @@ program2.command("find <query>").description("Find an epic or story by slug/titl
|
|
|
5334
5458
|
type: options.type
|
|
5335
5459
|
});
|
|
5336
5460
|
});
|
|
5461
|
+
program2.command("worktree <epic-slug> <story-slug>").description("Create git worktree for a story").action(async (epicSlug, storySlug) => {
|
|
5462
|
+
const globalOpts = program2.opts();
|
|
5463
|
+
await worktreeCommand(epicSlug, storySlug, { path: globalOpts.path });
|
|
5464
|
+
});
|
|
5337
5465
|
program2.command("dashboard").description("Start the dashboard server").option("--port <n>", "Port to run the server on (default: 3847)", parseInt).action(async (options) => {
|
|
5338
5466
|
const globalOpts = program2.opts();
|
|
5339
5467
|
await dashboardCommand({
|