@saga-ai/cli 0.1.0 → 0.1.2
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 +11 -7
- package/dist/cli.cjs +332 -37
- package/package.json +10 -9
- package/dist/cli.js +0 -3063
package/README.md
CHANGED
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Command-line interface for SAGA (Structured Autonomous Goal Achievement) - a structured development workflow that combines human-guided epic planning with autonomous story execution.
|
|
4
4
|
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g @saga-ai/cli
|
|
9
|
-
```
|
|
10
|
-
|
|
11
5
|
## Requirements
|
|
12
6
|
|
|
13
7
|
- Node.js >= 18.0.0
|
|
14
|
-
- [Claude
|
|
8
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (for `implement` command)
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
Run commands using npx (no global installation required):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @saga-ai/cli init
|
|
16
|
+
npx @saga-ai/cli implement <story-slug>
|
|
17
|
+
npx @saga-ai/cli dashboard
|
|
18
|
+
```
|
|
15
19
|
|
|
16
20
|
## Commands
|
|
17
21
|
|
package/dist/cli.cjs
CHANGED
|
@@ -26,9 +26,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
mod
|
|
27
27
|
));
|
|
28
28
|
|
|
29
|
-
// node_modules/commander/lib/error.js
|
|
29
|
+
// node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/error.js
|
|
30
30
|
var require_error = __commonJS({
|
|
31
|
-
"node_modules/commander/lib/error.js"(exports2) {
|
|
31
|
+
"node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/error.js"(exports2) {
|
|
32
32
|
var CommanderError2 = class extends Error {
|
|
33
33
|
/**
|
|
34
34
|
* Constructs the CommanderError class
|
|
@@ -61,9 +61,9 @@ var require_error = __commonJS({
|
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
// node_modules/commander/lib/argument.js
|
|
64
|
+
// node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/argument.js
|
|
65
65
|
var require_argument = __commonJS({
|
|
66
|
-
"node_modules/commander/lib/argument.js"(exports2) {
|
|
66
|
+
"node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/argument.js"(exports2) {
|
|
67
67
|
var { InvalidArgumentError: InvalidArgumentError2 } = require_error();
|
|
68
68
|
var Argument2 = class {
|
|
69
69
|
/**
|
|
@@ -188,9 +188,9 @@ var require_argument = __commonJS({
|
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
-
// node_modules/commander/lib/help.js
|
|
191
|
+
// node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/help.js
|
|
192
192
|
var require_help = __commonJS({
|
|
193
|
-
"node_modules/commander/lib/help.js"(exports2) {
|
|
193
|
+
"node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/help.js"(exports2) {
|
|
194
194
|
var { humanReadableArgName } = require_argument();
|
|
195
195
|
var Help2 = class {
|
|
196
196
|
constructor() {
|
|
@@ -602,9 +602,9 @@ var require_help = __commonJS({
|
|
|
602
602
|
}
|
|
603
603
|
});
|
|
604
604
|
|
|
605
|
-
// node_modules/commander/lib/option.js
|
|
605
|
+
// node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/option.js
|
|
606
606
|
var require_option = __commonJS({
|
|
607
|
-
"node_modules/commander/lib/option.js"(exports2) {
|
|
607
|
+
"node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/option.js"(exports2) {
|
|
608
608
|
var { InvalidArgumentError: InvalidArgumentError2 } = require_error();
|
|
609
609
|
var Option2 = class {
|
|
610
610
|
/**
|
|
@@ -874,9 +874,9 @@ var require_option = __commonJS({
|
|
|
874
874
|
}
|
|
875
875
|
});
|
|
876
876
|
|
|
877
|
-
// node_modules/commander/lib/suggestSimilar.js
|
|
877
|
+
// node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/suggestSimilar.js
|
|
878
878
|
var require_suggestSimilar = __commonJS({
|
|
879
|
-
"node_modules/commander/lib/suggestSimilar.js"(exports2) {
|
|
879
|
+
"node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/suggestSimilar.js"(exports2) {
|
|
880
880
|
var maxDistance = 3;
|
|
881
881
|
function editDistance(a, b) {
|
|
882
882
|
if (Math.abs(a.length - b.length) > maxDistance)
|
|
@@ -954,9 +954,9 @@ var require_suggestSimilar = __commonJS({
|
|
|
954
954
|
}
|
|
955
955
|
});
|
|
956
956
|
|
|
957
|
-
// node_modules/commander/lib/command.js
|
|
957
|
+
// node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js
|
|
958
958
|
var require_command = __commonJS({
|
|
959
|
-
"node_modules/commander/lib/command.js"(exports2) {
|
|
959
|
+
"node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js"(exports2) {
|
|
960
960
|
var EventEmitter = require("node:events").EventEmitter;
|
|
961
961
|
var childProcess = require("node:child_process");
|
|
962
962
|
var path = require("node:path");
|
|
@@ -2997,9 +2997,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2997
2997
|
}
|
|
2998
2998
|
});
|
|
2999
2999
|
|
|
3000
|
-
// node_modules/commander/index.js
|
|
3000
|
+
// node_modules/.pnpm/commander@12.1.0/node_modules/commander/index.js
|
|
3001
3001
|
var require_commander = __commonJS({
|
|
3002
|
-
"node_modules/commander/index.js"(exports2) {
|
|
3002
|
+
"node_modules/.pnpm/commander@12.1.0/node_modules/commander/index.js"(exports2) {
|
|
3003
3003
|
var { Argument: Argument2 } = require_argument();
|
|
3004
3004
|
var { Command: Command2 } = require_command();
|
|
3005
3005
|
var { CommanderError: CommanderError2, InvalidArgumentError: InvalidArgumentError2 } = require_error();
|
|
@@ -3019,7 +3019,7 @@ var require_commander = __commonJS({
|
|
|
3019
3019
|
}
|
|
3020
3020
|
});
|
|
3021
3021
|
|
|
3022
|
-
// node_modules/commander/esm.mjs
|
|
3022
|
+
// node_modules/.pnpm/commander@12.1.0/node_modules/commander/esm.mjs
|
|
3023
3023
|
var import_index = __toESM(require_commander(), 1);
|
|
3024
3024
|
var {
|
|
3025
3025
|
program,
|
|
@@ -3083,6 +3083,62 @@ Make sure the path points to a SAGA project root.`
|
|
|
3083
3083
|
|
|
3084
3084
|
// src/commands/init.ts
|
|
3085
3085
|
var WORKTREES_PATTERN = ".saga/worktrees/";
|
|
3086
|
+
function runInitDryRun(targetPath) {
|
|
3087
|
+
const sagaDir = (0, import_node_path2.join)(targetPath, ".saga");
|
|
3088
|
+
const sagaExists = (0, import_node_fs2.existsSync)(sagaDir);
|
|
3089
|
+
const directories = [
|
|
3090
|
+
{ name: "epics", path: (0, import_node_path2.join)(sagaDir, "epics") },
|
|
3091
|
+
{ name: "archive", path: (0, import_node_path2.join)(sagaDir, "archive") },
|
|
3092
|
+
{ name: "worktrees", path: (0, import_node_path2.join)(sagaDir, "worktrees") }
|
|
3093
|
+
].map((dir) => ({
|
|
3094
|
+
path: dir.path,
|
|
3095
|
+
exists: (0, import_node_fs2.existsSync)(dir.path),
|
|
3096
|
+
action: (0, import_node_fs2.existsSync)(dir.path) ? "exists (skip)" : "will create"
|
|
3097
|
+
}));
|
|
3098
|
+
const gitignorePath = (0, import_node_path2.join)(targetPath, ".gitignore");
|
|
3099
|
+
const gitignoreExists = (0, import_node_fs2.existsSync)(gitignorePath);
|
|
3100
|
+
let hasPattern = false;
|
|
3101
|
+
if (gitignoreExists) {
|
|
3102
|
+
const content = (0, import_node_fs2.readFileSync)(gitignorePath, "utf-8");
|
|
3103
|
+
hasPattern = content.includes(WORKTREES_PATTERN);
|
|
3104
|
+
}
|
|
3105
|
+
let gitignoreAction;
|
|
3106
|
+
if (!gitignoreExists) {
|
|
3107
|
+
gitignoreAction = "will create with worktrees pattern";
|
|
3108
|
+
} else if (hasPattern) {
|
|
3109
|
+
gitignoreAction = "already has pattern (skip)";
|
|
3110
|
+
} else {
|
|
3111
|
+
gitignoreAction = "will append worktrees pattern";
|
|
3112
|
+
}
|
|
3113
|
+
return {
|
|
3114
|
+
targetPath,
|
|
3115
|
+
sagaExists,
|
|
3116
|
+
directories,
|
|
3117
|
+
gitignore: {
|
|
3118
|
+
path: gitignorePath,
|
|
3119
|
+
exists: gitignoreExists,
|
|
3120
|
+
hasPattern,
|
|
3121
|
+
action: gitignoreAction
|
|
3122
|
+
}
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
function printInitDryRunResults(result) {
|
|
3126
|
+
console.log("Dry Run - Init Command");
|
|
3127
|
+
console.log("======================\n");
|
|
3128
|
+
console.log(`Target directory: ${result.targetPath}`);
|
|
3129
|
+
console.log(`SAGA project exists: ${result.sagaExists ? "yes" : "no"}
|
|
3130
|
+
`);
|
|
3131
|
+
console.log("Directories:");
|
|
3132
|
+
for (const dir of result.directories) {
|
|
3133
|
+
const icon = dir.exists ? "-" : "+";
|
|
3134
|
+
console.log(` ${icon} ${dir.path}`);
|
|
3135
|
+
console.log(` Action: ${dir.action}`);
|
|
3136
|
+
}
|
|
3137
|
+
console.log("\nGitignore:");
|
|
3138
|
+
console.log(` Path: ${result.gitignore.path}`);
|
|
3139
|
+
console.log(` Action: ${result.gitignore.action}`);
|
|
3140
|
+
console.log("\nDry run complete. No changes made.");
|
|
3141
|
+
}
|
|
3086
3142
|
function resolveTargetPath(explicitPath) {
|
|
3087
3143
|
if (explicitPath) {
|
|
3088
3144
|
return explicitPath;
|
|
@@ -3138,6 +3194,11 @@ async function initCommand(options) {
|
|
|
3138
3194
|
}
|
|
3139
3195
|
}
|
|
3140
3196
|
const targetPath = resolveTargetPath(options.path);
|
|
3197
|
+
if (options.dryRun) {
|
|
3198
|
+
const dryRunResult = runInitDryRun(targetPath);
|
|
3199
|
+
printInitDryRunResults(dryRunResult);
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3141
3202
|
createDirectoryStructure(targetPath);
|
|
3142
3203
|
updateGitignore(targetPath);
|
|
3143
3204
|
console.log(`Initialized .saga/ at ${targetPath}`);
|
|
@@ -3171,25 +3232,32 @@ var WORKER_OUTPUT_SCHEMA = {
|
|
|
3171
3232
|
required: ["status", "summary"]
|
|
3172
3233
|
};
|
|
3173
3234
|
function findStory(projectPath, storySlug) {
|
|
3174
|
-
const
|
|
3175
|
-
if (!(0, import_node_fs3.existsSync)(
|
|
3235
|
+
const worktreesDir = (0, import_node_path3.join)(projectPath, ".saga", "worktrees");
|
|
3236
|
+
if (!(0, import_node_fs3.existsSync)(worktreesDir)) {
|
|
3176
3237
|
return null;
|
|
3177
3238
|
}
|
|
3178
|
-
const epicDirs = (0, import_node_fs3.readdirSync)(
|
|
3239
|
+
const epicDirs = (0, import_node_fs3.readdirSync)(worktreesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
3179
3240
|
for (const epicSlug of epicDirs) {
|
|
3180
|
-
const
|
|
3181
|
-
|
|
3241
|
+
const epicWorktreeDir = (0, import_node_path3.join)(worktreesDir, epicSlug);
|
|
3242
|
+
const storyWorktree = (0, import_node_path3.join)(epicWorktreeDir, storySlug);
|
|
3243
|
+
if (!(0, import_node_fs3.existsSync)(storyWorktree)) {
|
|
3182
3244
|
continue;
|
|
3183
3245
|
}
|
|
3184
|
-
const
|
|
3185
|
-
|
|
3246
|
+
const storyPath = (0, import_node_path3.join)(
|
|
3247
|
+
storyWorktree,
|
|
3248
|
+
".saga",
|
|
3249
|
+
"epics",
|
|
3250
|
+
epicSlug,
|
|
3251
|
+
"stories",
|
|
3252
|
+
storySlug,
|
|
3253
|
+
"story.md"
|
|
3254
|
+
);
|
|
3186
3255
|
if ((0, import_node_fs3.existsSync)(storyPath)) {
|
|
3187
|
-
const worktreePath = (0, import_node_path3.join)(projectPath, ".saga", "worktrees", epicSlug, storySlug);
|
|
3188
3256
|
return {
|
|
3189
3257
|
epicSlug,
|
|
3190
3258
|
storySlug,
|
|
3191
3259
|
storyPath,
|
|
3192
|
-
worktreePath
|
|
3260
|
+
worktreePath: storyWorktree
|
|
3193
3261
|
};
|
|
3194
3262
|
}
|
|
3195
3263
|
}
|
|
@@ -3228,6 +3296,135 @@ This may indicate an incomplete story setup.`
|
|
|
3228
3296
|
function getSkillRoot(pluginRoot) {
|
|
3229
3297
|
return (0, import_node_path3.join)(pluginRoot, "skills", "execute-story");
|
|
3230
3298
|
}
|
|
3299
|
+
function checkCommandExists(command) {
|
|
3300
|
+
try {
|
|
3301
|
+
const result = (0, import_node_child_process.spawnSync)("which", [command], { encoding: "utf-8" });
|
|
3302
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
3303
|
+
return { exists: true, path: result.stdout.trim() };
|
|
3304
|
+
}
|
|
3305
|
+
return { exists: false };
|
|
3306
|
+
} catch {
|
|
3307
|
+
return { exists: false };
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
function runDryRun(storyInfo, projectPath, pluginRoot) {
|
|
3311
|
+
const checks = [];
|
|
3312
|
+
let allPassed = true;
|
|
3313
|
+
if (pluginRoot) {
|
|
3314
|
+
checks.push({
|
|
3315
|
+
name: "SAGA_PLUGIN_ROOT",
|
|
3316
|
+
path: pluginRoot,
|
|
3317
|
+
passed: true
|
|
3318
|
+
});
|
|
3319
|
+
} else {
|
|
3320
|
+
checks.push({
|
|
3321
|
+
name: "SAGA_PLUGIN_ROOT",
|
|
3322
|
+
passed: false,
|
|
3323
|
+
error: "Environment variable not set"
|
|
3324
|
+
});
|
|
3325
|
+
allPassed = false;
|
|
3326
|
+
}
|
|
3327
|
+
const claudeCheck = checkCommandExists("claude");
|
|
3328
|
+
checks.push({
|
|
3329
|
+
name: "claude CLI",
|
|
3330
|
+
path: claudeCheck.path,
|
|
3331
|
+
passed: claudeCheck.exists,
|
|
3332
|
+
error: claudeCheck.exists ? void 0 : "Command not found in PATH"
|
|
3333
|
+
});
|
|
3334
|
+
if (!claudeCheck.exists) allPassed = false;
|
|
3335
|
+
if (pluginRoot) {
|
|
3336
|
+
const skillRoot = getSkillRoot(pluginRoot);
|
|
3337
|
+
const workerPromptPath = (0, import_node_path3.join)(skillRoot, WORKER_PROMPT_RELATIVE);
|
|
3338
|
+
if ((0, import_node_fs3.existsSync)(workerPromptPath)) {
|
|
3339
|
+
checks.push({
|
|
3340
|
+
name: "Worker prompt",
|
|
3341
|
+
path: workerPromptPath,
|
|
3342
|
+
passed: true
|
|
3343
|
+
});
|
|
3344
|
+
} else {
|
|
3345
|
+
checks.push({
|
|
3346
|
+
name: "Worker prompt",
|
|
3347
|
+
path: workerPromptPath,
|
|
3348
|
+
passed: false,
|
|
3349
|
+
error: "File not found"
|
|
3350
|
+
});
|
|
3351
|
+
allPassed = false;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
checks.push({
|
|
3355
|
+
name: "Story found",
|
|
3356
|
+
path: `${storyInfo.storySlug} (epic: ${storyInfo.epicSlug})`,
|
|
3357
|
+
passed: true
|
|
3358
|
+
});
|
|
3359
|
+
if ((0, import_node_fs3.existsSync)(storyInfo.worktreePath)) {
|
|
3360
|
+
checks.push({
|
|
3361
|
+
name: "Worktree exists",
|
|
3362
|
+
path: storyInfo.worktreePath,
|
|
3363
|
+
passed: true
|
|
3364
|
+
});
|
|
3365
|
+
} else {
|
|
3366
|
+
checks.push({
|
|
3367
|
+
name: "Worktree exists",
|
|
3368
|
+
path: storyInfo.worktreePath,
|
|
3369
|
+
passed: false,
|
|
3370
|
+
error: "Directory not found"
|
|
3371
|
+
});
|
|
3372
|
+
allPassed = false;
|
|
3373
|
+
}
|
|
3374
|
+
if ((0, import_node_fs3.existsSync)(storyInfo.worktreePath)) {
|
|
3375
|
+
const storyMdPath = computeStoryPath(
|
|
3376
|
+
storyInfo.worktreePath,
|
|
3377
|
+
storyInfo.epicSlug,
|
|
3378
|
+
storyInfo.storySlug
|
|
3379
|
+
);
|
|
3380
|
+
if ((0, import_node_fs3.existsSync)(storyMdPath)) {
|
|
3381
|
+
checks.push({
|
|
3382
|
+
name: "story.md in worktree",
|
|
3383
|
+
path: storyMdPath,
|
|
3384
|
+
passed: true
|
|
3385
|
+
});
|
|
3386
|
+
} else {
|
|
3387
|
+
checks.push({
|
|
3388
|
+
name: "story.md in worktree",
|
|
3389
|
+
path: storyMdPath,
|
|
3390
|
+
passed: false,
|
|
3391
|
+
error: "File not found"
|
|
3392
|
+
});
|
|
3393
|
+
allPassed = false;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
return {
|
|
3397
|
+
success: allPassed,
|
|
3398
|
+
checks,
|
|
3399
|
+
story: {
|
|
3400
|
+
epicSlug: storyInfo.epicSlug,
|
|
3401
|
+
storySlug: storyInfo.storySlug,
|
|
3402
|
+
worktreePath: storyInfo.worktreePath
|
|
3403
|
+
}
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
function printDryRunResults(result) {
|
|
3407
|
+
console.log("Dry Run Validation");
|
|
3408
|
+
console.log("==================\n");
|
|
3409
|
+
for (const check of result.checks) {
|
|
3410
|
+
const icon = check.passed ? "\u2713" : "\u2717";
|
|
3411
|
+
const status = check.passed ? "OK" : "FAILED";
|
|
3412
|
+
if (check.passed) {
|
|
3413
|
+
console.log(`${icon} ${check.name}: ${check.path || status}`);
|
|
3414
|
+
} else {
|
|
3415
|
+
console.log(`${icon} ${check.name}: ${check.error}`);
|
|
3416
|
+
if (check.path) {
|
|
3417
|
+
console.log(` Expected: ${check.path}`);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
console.log("");
|
|
3422
|
+
if (result.success) {
|
|
3423
|
+
console.log("Dry run complete. All checks passed. Ready to implement.");
|
|
3424
|
+
} else {
|
|
3425
|
+
console.log("Dry run failed. Please fix the issues above before running implement.");
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3231
3428
|
function loadWorkerPrompt(pluginRoot) {
|
|
3232
3429
|
const skillRoot = getSkillRoot(pluginRoot);
|
|
3233
3430
|
const promptPath = (0, import_node_path3.join)(skillRoot, WORKER_PROMPT_RELATIVE);
|
|
@@ -3236,10 +3433,8 @@ function loadWorkerPrompt(pluginRoot) {
|
|
|
3236
3433
|
}
|
|
3237
3434
|
return (0, import_node_fs3.readFileSync)(promptPath, "utf-8");
|
|
3238
3435
|
}
|
|
3239
|
-
function buildScopeSettings(
|
|
3240
|
-
const
|
|
3241
|
-
const validatorPath = (0, import_node_path3.join)(skillRoot, "scripts", "scope_validator.py");
|
|
3242
|
-
const hookCommand = `python3 ${validatorPath}`;
|
|
3436
|
+
function buildScopeSettings() {
|
|
3437
|
+
const hookCommand = "npx @saga-ai/cli scope-validator";
|
|
3243
3438
|
return {
|
|
3244
3439
|
hooks: {
|
|
3245
3440
|
PreToolUse: [
|
|
@@ -3337,7 +3532,7 @@ function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, plu
|
|
|
3337
3532
|
storySlug
|
|
3338
3533
|
};
|
|
3339
3534
|
}
|
|
3340
|
-
const settings = buildScopeSettings(
|
|
3535
|
+
const settings = buildScopeSettings();
|
|
3341
3536
|
const startTime = Date.now();
|
|
3342
3537
|
let cycles = 0;
|
|
3343
3538
|
const summaries = [];
|
|
@@ -3416,17 +3611,23 @@ async function implementCommand(storySlug, options) {
|
|
|
3416
3611
|
if (!storyInfo) {
|
|
3417
3612
|
console.error(`Error: Story '${storySlug}' not found in SAGA project.`);
|
|
3418
3613
|
console.error(`
|
|
3419
|
-
Searched in: ${(0, import_node_path3.join)(projectPath, ".saga", "
|
|
3420
|
-
console.error("\nMake sure the story exists and has a story.md file.");
|
|
3614
|
+
Searched in: ${(0, import_node_path3.join)(projectPath, ".saga", "worktrees")}`);
|
|
3615
|
+
console.error("\nMake sure the story worktree exists and has a story.md file.");
|
|
3616
|
+
console.error("Run /generate-stories to create story worktrees.");
|
|
3421
3617
|
process.exit(1);
|
|
3422
3618
|
}
|
|
3619
|
+
const pluginRoot = process.env.SAGA_PLUGIN_ROOT;
|
|
3620
|
+
if (options.dryRun) {
|
|
3621
|
+
const dryRunResult = runDryRun(storyInfo, projectPath, pluginRoot);
|
|
3622
|
+
printDryRunResults(dryRunResult);
|
|
3623
|
+
process.exit(dryRunResult.success ? 0 : 1);
|
|
3624
|
+
}
|
|
3423
3625
|
if (!(0, import_node_fs3.existsSync)(storyInfo.worktreePath)) {
|
|
3424
3626
|
console.error(`Error: Worktree not found at ${storyInfo.worktreePath}`);
|
|
3425
3627
|
console.error("\nThe story worktree has not been created yet.");
|
|
3426
3628
|
console.error("Make sure the story was properly generated with /generate-stories.");
|
|
3427
3629
|
process.exit(1);
|
|
3428
3630
|
}
|
|
3429
|
-
const pluginRoot = process.env.SAGA_PLUGIN_ROOT;
|
|
3430
3631
|
if (!pluginRoot) {
|
|
3431
3632
|
console.error("Error: SAGA_PLUGIN_ROOT environment variable is not set.");
|
|
3432
3633
|
console.error("\nThis variable is required for the implementation script.");
|
|
@@ -3476,23 +3677,114 @@ async function dashboardCommand(options) {
|
|
|
3476
3677
|
console.log("Note: Dashboard server implementation pending (Backend Server story)");
|
|
3477
3678
|
}
|
|
3478
3679
|
|
|
3680
|
+
// src/commands/scope-validator.ts
|
|
3681
|
+
function getFilePathFromInput(toolInput) {
|
|
3682
|
+
try {
|
|
3683
|
+
const data = JSON.parse(toolInput);
|
|
3684
|
+
return data.file_path || data.path || null;
|
|
3685
|
+
} catch {
|
|
3686
|
+
return null;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
function normalizePath(path) {
|
|
3690
|
+
if (path.startsWith("./")) {
|
|
3691
|
+
return path.slice(2);
|
|
3692
|
+
}
|
|
3693
|
+
return path;
|
|
3694
|
+
}
|
|
3695
|
+
function isArchiveAccess(path) {
|
|
3696
|
+
return path.includes(".saga/archive");
|
|
3697
|
+
}
|
|
3698
|
+
function checkStoryAccess(path, allowedEpic, allowedStory) {
|
|
3699
|
+
if (!path.includes(".saga/epics/")) {
|
|
3700
|
+
return true;
|
|
3701
|
+
}
|
|
3702
|
+
const parts = path.split("/");
|
|
3703
|
+
const epicsIdx = parts.indexOf("epics");
|
|
3704
|
+
if (epicsIdx === -1) {
|
|
3705
|
+
return true;
|
|
3706
|
+
}
|
|
3707
|
+
if (parts.length <= epicsIdx + 1) {
|
|
3708
|
+
return true;
|
|
3709
|
+
}
|
|
3710
|
+
const pathEpic = parts[epicsIdx + 1];
|
|
3711
|
+
if (parts.length > epicsIdx + 3 && parts[epicsIdx + 2] === "stories") {
|
|
3712
|
+
const pathStory = parts[epicsIdx + 3];
|
|
3713
|
+
return pathEpic === allowedEpic && pathStory === allowedStory;
|
|
3714
|
+
} else {
|
|
3715
|
+
return pathEpic === allowedEpic;
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
function printScopeViolation(filePath, epicSlug, storySlug, reason) {
|
|
3719
|
+
console.error(`SCOPE VIOLATION: ${reason}
|
|
3720
|
+
|
|
3721
|
+
Attempted path: ${filePath}
|
|
3722
|
+
|
|
3723
|
+
Your scope is limited to:
|
|
3724
|
+
Epic: ${epicSlug}
|
|
3725
|
+
Story: ${storySlug}
|
|
3726
|
+
Allowed: .saga/epics/${epicSlug}/stories/${storySlug}/
|
|
3727
|
+
|
|
3728
|
+
To access other stories, start a new /implement session for that story.`);
|
|
3729
|
+
}
|
|
3730
|
+
async function scopeValidatorCommand() {
|
|
3731
|
+
const epicSlug = process.env.SAGA_EPIC_SLUG || "";
|
|
3732
|
+
const storySlug = process.env.SAGA_STORY_SLUG || "";
|
|
3733
|
+
if (!epicSlug || !storySlug) {
|
|
3734
|
+
console.error(
|
|
3735
|
+
"ERROR: scope-validator requires SAGA_EPIC_SLUG and SAGA_STORY_SLUG environment variables"
|
|
3736
|
+
);
|
|
3737
|
+
process.exit(2);
|
|
3738
|
+
}
|
|
3739
|
+
const chunks = [];
|
|
3740
|
+
for await (const chunk of process.stdin) {
|
|
3741
|
+
chunks.push(chunk);
|
|
3742
|
+
}
|
|
3743
|
+
const toolInput = Buffer.concat(chunks).toString("utf-8");
|
|
3744
|
+
const filePath = getFilePathFromInput(toolInput);
|
|
3745
|
+
if (!filePath) {
|
|
3746
|
+
process.exit(0);
|
|
3747
|
+
}
|
|
3748
|
+
const normPath = normalizePath(filePath);
|
|
3749
|
+
if (isArchiveAccess(normPath)) {
|
|
3750
|
+
printScopeViolation(
|
|
3751
|
+
filePath,
|
|
3752
|
+
epicSlug,
|
|
3753
|
+
storySlug,
|
|
3754
|
+
"Access to archive folder blocked\nReason: The archive folder contains completed stories and is read-only during execution."
|
|
3755
|
+
);
|
|
3756
|
+
process.exit(2);
|
|
3757
|
+
}
|
|
3758
|
+
if (!checkStoryAccess(normPath, epicSlug, storySlug)) {
|
|
3759
|
+
printScopeViolation(
|
|
3760
|
+
filePath,
|
|
3761
|
+
epicSlug,
|
|
3762
|
+
storySlug,
|
|
3763
|
+
"Access to other story blocked\nReason: Workers can only access their assigned story's files."
|
|
3764
|
+
);
|
|
3765
|
+
process.exit(2);
|
|
3766
|
+
}
|
|
3767
|
+
process.exit(0);
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3479
3770
|
// src/cli.ts
|
|
3480
3771
|
var packageJsonPath = (0, import_node_path4.join)(__dirname, "..", "package.json");
|
|
3481
3772
|
var packageJson = JSON.parse((0, import_node_fs4.readFileSync)(packageJsonPath, "utf-8"));
|
|
3482
3773
|
var program2 = new Command();
|
|
3483
|
-
program2.name("saga").description("CLI for SAGA - Structured Autonomous Goal Achievement").version(packageJson.version);
|
|
3774
|
+
program2.name("saga").description("CLI for SAGA - Structured Autonomous Goal Achievement").version(packageJson.version).addHelpCommand("help [command]", "Display help for a command");
|
|
3484
3775
|
program2.option("-p, --path <dir>", "Path to SAGA project directory (overrides auto-discovery)");
|
|
3485
|
-
program2.command("init").description("Initialize .saga/ directory structure").action(async () => {
|
|
3776
|
+
program2.command("init").description("Initialize .saga/ directory structure").option("--dry-run", "Show what would be created without making changes").action(async (options) => {
|
|
3486
3777
|
const globalOpts = program2.opts();
|
|
3487
|
-
await initCommand({ path: globalOpts.path });
|
|
3778
|
+
await initCommand({ path: globalOpts.path, dryRun: options.dryRun });
|
|
3488
3779
|
});
|
|
3489
|
-
program2.command("implement <story-slug>").description("Run story implementation").option("--max-cycles <n>", "Maximum number of implementation cycles", parseInt).option("--max-time <n>", "Maximum time in minutes", parseInt).option("--model <name>", "Model to use for implementation").action(async (storySlug, options) => {
|
|
3780
|
+
program2.command("implement <story-slug>").description("Run story implementation").option("--max-cycles <n>", "Maximum number of implementation cycles", parseInt).option("--max-time <n>", "Maximum time in minutes", parseInt).option("--model <name>", "Model to use for implementation").option("--dry-run", "Validate dependencies without running implementation").action(async (storySlug, options) => {
|
|
3490
3781
|
const globalOpts = program2.opts();
|
|
3491
3782
|
await implementCommand(storySlug, {
|
|
3492
3783
|
path: globalOpts.path,
|
|
3493
3784
|
maxCycles: options.maxCycles,
|
|
3494
3785
|
maxTime: options.maxTime,
|
|
3495
|
-
model: options.model
|
|
3786
|
+
model: options.model,
|
|
3787
|
+
dryRun: options.dryRun
|
|
3496
3788
|
});
|
|
3497
3789
|
});
|
|
3498
3790
|
program2.command("dashboard").description("Start the dashboard server").option("--port <n>", "Port to run the server on (default: 3847)", parseInt).action(async (options) => {
|
|
@@ -3502,6 +3794,9 @@ program2.command("dashboard").description("Start the dashboard server").option("
|
|
|
3502
3794
|
port: options.port
|
|
3503
3795
|
});
|
|
3504
3796
|
});
|
|
3797
|
+
program2.command("scope-validator").description("Validate file operations against story scope (internal)").action(async () => {
|
|
3798
|
+
await scopeValidatorCommand();
|
|
3799
|
+
});
|
|
3505
3800
|
program2.on("command:*", (operands) => {
|
|
3506
3801
|
console.error(`error: unknown command '${operands[0]}'`);
|
|
3507
3802
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saga-ai/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "CLI for SAGA - Structured Autonomous Goal Achievement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"saga": "./dist/cli.cjs"
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/cli.cjs",
|
|
10
|
-
"scripts": {
|
|
11
|
-
"build": "esbuild src/cli.ts --bundle --platform=node --outfile=dist/cli.cjs --format=cjs --banner:js='#!/usr/bin/env node'",
|
|
12
|
-
"dev": "npm run build && node dist/cli.js",
|
|
13
|
-
"test": "vitest run",
|
|
14
|
-
"test:watch": "vitest"
|
|
15
|
-
},
|
|
16
10
|
"keywords": [
|
|
17
11
|
"claude",
|
|
18
12
|
"saga",
|
|
@@ -43,5 +37,12 @@
|
|
|
43
37
|
"files": [
|
|
44
38
|
"dist/",
|
|
45
39
|
"README.md"
|
|
46
|
-
]
|
|
47
|
-
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "esbuild src/cli.ts --bundle --platform=node --outfile=dist/cli.cjs --format=cjs --banner:js='#!/usr/bin/env node'",
|
|
43
|
+
"dev": "npm run build && node dist/cli.js",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"publish:npm": "pnpm build && pnpm test && pnpm publish --access public"
|
|
47
|
+
}
|
|
48
|
+
}
|