sdd-forge 0.1.0-alpha.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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/package.json +23 -0
  4. package/src/analyzers/analyze-controllers.js +85 -0
  5. package/src/analyzers/analyze-extras.js +944 -0
  6. package/src/analyzers/analyze-models.js +130 -0
  7. package/src/analyzers/analyze-routes.js +50 -0
  8. package/src/analyzers/analyze-shells.js +75 -0
  9. package/src/analyzers/lib/php-array-parser.js +138 -0
  10. package/src/analyzers/scan.js +116 -0
  11. package/src/bin/sdd-forge.js +117 -0
  12. package/src/engine/directive-parser.js +72 -0
  13. package/src/engine/init.js +253 -0
  14. package/src/engine/populate.js +192 -0
  15. package/src/engine/readme.js +174 -0
  16. package/src/engine/renderers.js +150 -0
  17. package/src/engine/resolver.js +568 -0
  18. package/src/engine/tfill.js +617 -0
  19. package/src/flow/flow.js +183 -0
  20. package/src/forge/forge.js +684 -0
  21. package/src/help.js +55 -0
  22. package/src/lib/cli.js +83 -0
  23. package/src/lib/config.js +40 -0
  24. package/src/lib/process.js +32 -0
  25. package/src/projects/add.js +73 -0
  26. package/src/projects/projects.js +91 -0
  27. package/src/projects/setdefault.js +37 -0
  28. package/src/spec/gate.js +101 -0
  29. package/src/spec/spec.js +198 -0
  30. package/src/templates/checks/check-controller-coverage.sh +46 -0
  31. package/src/templates/checks/check-db-coverage.sh +87 -0
  32. package/src/templates/checks/check-node-cli-docs.sh +125 -0
  33. package/src/templates/checks/check-temp-docs.sh +100 -0
  34. package/src/templates/checks/generate-change-log.sh +142 -0
  35. package/src/templates/checks/self-review-temp-docs.sh +18 -0
  36. package/src/templates/locale/ja/messages.json +9 -0
  37. package/src/templates/locale/ja/node-cli/01_overview.md +23 -0
  38. package/src/templates/locale/ja/node-cli/02_cli_commands.md +23 -0
  39. package/src/templates/locale/ja/node-cli/03_configuration.md +23 -0
  40. package/src/templates/locale/ja/node-cli/04_internal_design.md +30 -0
  41. package/src/templates/locale/ja/node-cli/05_development.md +49 -0
  42. package/src/templates/locale/ja/node-cli/README.md +41 -0
  43. package/src/templates/locale/ja/php-mvc/01_architecture.md +23 -0
  44. package/src/templates/locale/ja/php-mvc/02_stack_and_ops.md +45 -0
  45. package/src/templates/locale/ja/php-mvc/03_project_structure.md +46 -0
  46. package/src/templates/locale/ja/php-mvc/04_development.md +69 -0
  47. package/src/templates/locale/ja/php-mvc/05_auth_and_session.md +48 -0
  48. package/src/templates/locale/ja/php-mvc/06_database_architecture.md +23 -0
  49. package/src/templates/locale/ja/php-mvc/07_db_tables.md +31 -0
  50. package/src/templates/locale/ja/php-mvc/08_controller_routes.md +33 -0
  51. package/src/templates/locale/ja/php-mvc/09_business_logic.md +27 -0
  52. package/src/templates/locale/ja/php-mvc/10_batch_and_shell.md +25 -0
  53. package/src/templates/locale/ja/php-mvc/README.md +34 -0
  54. package/src/templates/locale/ja/prompts.json +4 -0
  55. package/src/templates/locale/ja/sections.json +6 -0
  56. package/src/templates/review-checklist.md +48 -0
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tools/analyzers/analyze-models.js
4
+ *
5
+ * app/Model/*.php + app/Model/Logic/*.php を走査し、
6
+ * テーブル名・DB設定・リレーション・バリデーション等を抽出する。
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import {
12
+ stripBlockComments,
13
+ extractArrayBody,
14
+ extractTopLevelKeys,
15
+ extractQuotedStrings,
16
+ camelToSnake,
17
+ pluralize,
18
+ } from "./lib/php-array-parser.js";
19
+
20
+ const RELATION_TYPES = [
21
+ "belongsTo",
22
+ "hasMany",
23
+ "hasOne",
24
+ "hasAndBelongsToMany",
25
+ ];
26
+
27
+ function extractStringProperty(src, propertyName) {
28
+ const escaped = propertyName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29
+ const re = new RegExp(
30
+ `(?:var|public|protected|private)\\s+\\$${escaped}\\s*=\\s*['"]([^'"]+)['"]`,
31
+ );
32
+ const m = re.exec(src);
33
+ return m ? m[1] : null;
34
+ }
35
+
36
+ function inferTableName(className) {
37
+ const snake = camelToSnake(className);
38
+ return pluralize(snake);
39
+ }
40
+
41
+ export function analyzeModels(appDir) {
42
+ const dirs = [
43
+ { dir: path.join(appDir, "Model"), isLogic: false },
44
+ { dir: path.join(appDir, "Model", "Logic"), isLogic: true },
45
+ ];
46
+
47
+ const models = [];
48
+
49
+ for (const { dir, isLogic } of dirs) {
50
+ if (!fs.existsSync(dir)) continue;
51
+
52
+ const files = fs
53
+ .readdirSync(dir)
54
+ .filter((f) => f.endsWith(".php") && f !== "AppModel.php");
55
+
56
+ for (const file of files) {
57
+ const filePath = path.join(dir, file);
58
+ const stat = fs.statSync(filePath);
59
+ if (!stat.isFile()) continue;
60
+
61
+ const raw = fs.readFileSync(filePath, "utf8");
62
+ const src = stripBlockComments(raw);
63
+
64
+ const classMatch = src.match(/class\s+(\w+)\s+extends\s+(\w+)/);
65
+ if (!classMatch) continue;
66
+
67
+ const className = classMatch[1];
68
+ const parentClass = classMatch[2];
69
+
70
+ const useTable = extractStringProperty(src, "useTable");
71
+ const useDbConfig = extractStringProperty(src, "useDbConfig");
72
+ const primaryKey = extractStringProperty(src, "primaryKey");
73
+ const displayField = extractStringProperty(src, "displayField");
74
+
75
+ const relations = {};
76
+ for (const relType of RELATION_TYPES) {
77
+ const body = extractArrayBody(src, relType);
78
+ if (body) {
79
+ relations[relType] = extractTopLevelKeys(body);
80
+ }
81
+ }
82
+
83
+ const validateBody = extractArrayBody(src, "validate");
84
+ const validateFields = validateBody
85
+ ? extractTopLevelKeys(validateBody)
86
+ : [];
87
+
88
+ const actsAsBody = extractArrayBody(src, "actsAs");
89
+ const actsAs = actsAsBody ? extractQuotedStrings(actsAsBody) : [];
90
+
91
+ const tableName = useTable || inferTableName(className);
92
+ const isFe = className.startsWith("Fe");
93
+
94
+ models.push({
95
+ file: path.relative(path.resolve(appDir, ".."), filePath),
96
+ className,
97
+ parentClass,
98
+ isLogic,
99
+ isFe,
100
+ useTable: useTable || null,
101
+ useDbConfig: useDbConfig || null,
102
+ primaryKey: primaryKey || null,
103
+ displayField: displayField || null,
104
+ tableName,
105
+ relations,
106
+ validateFields,
107
+ actsAs,
108
+ });
109
+ }
110
+ }
111
+
112
+ models.sort((a, b) => a.className.localeCompare(b.className));
113
+
114
+ const dbGroups = {};
115
+ for (const m of models) {
116
+ const db = m.useDbConfig || "default";
117
+ if (!dbGroups[db]) dbGroups[db] = [];
118
+ dbGroups[db].push(m.className);
119
+ }
120
+
121
+ return {
122
+ models,
123
+ summary: {
124
+ total: models.length,
125
+ feModels: models.filter((m) => m.isFe).length,
126
+ logicModels: models.filter((m) => m.isLogic).length,
127
+ dbGroups,
128
+ },
129
+ };
130
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tools/analyzers/analyze-routes.js
4
+ *
5
+ * app/Config/routes.php を読み込み、
6
+ * Router::connect() パターンを抽出する。
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { stripBlockComments } from "./lib/php-array-parser.js";
12
+
13
+ export function analyzeRoutes(appDir) {
14
+ const routesPath = path.join(appDir, "Config", "routes.php");
15
+ if (!fs.existsSync(routesPath)) return { routes: [], summary: {} };
16
+
17
+ const raw = fs.readFileSync(routesPath, "utf8");
18
+ const src = stripBlockComments(raw);
19
+
20
+ const routes = [];
21
+ const re =
22
+ /Router::connect\s*\(\s*['"]([^'"]*)['"]\s*,\s*array\s*\(([^)]*)\)/g;
23
+ let m;
24
+ while ((m = re.exec(src)) !== null) {
25
+ const pattern = m[1];
26
+ const paramsStr = m[2];
27
+
28
+ const controllerMatch = paramsStr.match(
29
+ /['"]controller['"]\s*=>\s*['"](\w+)['"]/,
30
+ );
31
+ const actionMatch = paramsStr.match(
32
+ /['"]action['"]\s*=>\s*['"](\w+)['"]/,
33
+ );
34
+
35
+ routes.push({
36
+ pattern,
37
+ controller: controllerMatch ? controllerMatch[1] : null,
38
+ action: actionMatch ? actionMatch[1] : null,
39
+ raw: `Router::connect('${pattern}', array(${paramsStr.trim()}))`,
40
+ });
41
+ }
42
+
43
+ return {
44
+ routes,
45
+ summary: {
46
+ total: routes.length,
47
+ controllers: [...new Set(routes.map((r) => r.controller).filter(Boolean))],
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * tools/analyzers/analyze-shells.js
4
+ *
5
+ * app/Console/Command/*Shell.php を走査し、
6
+ * クラス名・公開メソッド・main 有無・App::uses を抽出する。
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { stripBlockComments } from "./lib/php-array-parser.js";
12
+
13
+ const LIFECYCLE_METHODS = new Set([
14
+ "startup",
15
+ "initialize",
16
+ "getOptionParser",
17
+ ]);
18
+
19
+ export function analyzeShells(appDir) {
20
+ const shellDir = path.join(appDir, "Console", "Command");
21
+ if (!fs.existsSync(shellDir)) return { shells: [], summary: {} };
22
+
23
+ const files = fs
24
+ .readdirSync(shellDir)
25
+ .filter((f) => f.endsWith("Shell.php") && f !== "AppShell.php");
26
+
27
+ const shells = [];
28
+ for (const file of files) {
29
+ const filePath = path.join(shellDir, file);
30
+ const raw = fs.readFileSync(filePath, "utf8");
31
+ const src = stripBlockComments(raw);
32
+
33
+ const classMatch = src.match(/class\s+(\w+)\s+extends\s+(\w+)/);
34
+ if (!classMatch) continue;
35
+
36
+ const className = classMatch[1];
37
+
38
+ const publicMethods = [];
39
+ const fnRe = /public\s+function\s+(\w+)\s*\(/g;
40
+ let fm;
41
+ while ((fm = fnRe.exec(src)) !== null) {
42
+ const name = fm[1];
43
+ if (!LIFECYCLE_METHODS.has(name) && !name.startsWith("_")) {
44
+ publicMethods.push(name);
45
+ }
46
+ }
47
+
48
+ const hasMain = publicMethods.includes("main");
49
+
50
+ const appUses = [];
51
+ const usesRe = /App::uses\s*\(\s*['"](\w+)['"]\s*,\s*['"]([\w/]+)['"]\s*\)/g;
52
+ let um;
53
+ while ((um = usesRe.exec(raw)) !== null) {
54
+ appUses.push({ class: um[1], package: um[2] });
55
+ }
56
+
57
+ shells.push({
58
+ file: path.relative(path.resolve(appDir, ".."), filePath),
59
+ className,
60
+ publicMethods,
61
+ hasMain,
62
+ appUses,
63
+ });
64
+ }
65
+
66
+ shells.sort((a, b) => a.className.localeCompare(b.className));
67
+
68
+ return {
69
+ shells,
70
+ summary: {
71
+ total: shells.length,
72
+ withMain: shells.filter((s) => s.hasMain).length,
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * tools/analyzers/lib/php-array-parser.js
3
+ *
4
+ * PHP ソースコード解析ユーティリティ。
5
+ * CakePHP 2.x の array() 構文からプロパティ・キー・値を抽出する。
6
+ */
7
+
8
+ /**
9
+ * ブロックコメント (/* ... *​/) を除去する。
10
+ * 行コメント (// ...) は除去しない。
11
+ */
12
+ export function stripBlockComments(text) {
13
+ return text.replace(/\/\*[\s\S]*?\*\//g, "");
14
+ }
15
+
16
+ /**
17
+ * `$property = array(...)` の中身を括弧バランスで抽出する。
18
+ * `var`, `public`, `protected`, `private` 修飾子に対応。
19
+ * 見つからない場合は空文字を返す。
20
+ */
21
+ export function extractArrayBody(fileContent, propertyName) {
22
+ const escaped = propertyName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ const re = new RegExp(
24
+ `(?:var|public|protected|private)\\s+\\$${escaped}\\s*=\\s*array\\s*\\(`,
25
+ "m",
26
+ );
27
+ const m = re.exec(fileContent);
28
+ if (!m) return "";
29
+
30
+ const startIdx = m.index + m[0].length;
31
+ let depth = 1;
32
+ let i = startIdx;
33
+ while (i < fileContent.length && depth > 0) {
34
+ const ch = fileContent[i];
35
+ if (ch === "(") depth += 1;
36
+ else if (ch === ")") depth -= 1;
37
+ i += 1;
38
+ }
39
+ if (depth !== 0) return "";
40
+ return fileContent.slice(startIdx, i - 1);
41
+ }
42
+
43
+ /**
44
+ * 連想配列の第一階層キーを抽出する。
45
+ * `'Key' => array(...)` パターンから Key 部分を返す。
46
+ */
47
+ export function extractTopLevelKeys(arrayBody) {
48
+ const keys = [];
49
+ const re = /['"](\w+)['"]\s*=>/g;
50
+ let depth = 0;
51
+ let lastIdx = 0;
52
+
53
+ for (let i = 0; i < arrayBody.length; i += 1) {
54
+ const ch = arrayBody[i];
55
+ if (ch === "(" || ch === "[") {
56
+ depth += 1;
57
+ } else if (ch === ")" || ch === "]") {
58
+ depth -= 1;
59
+ }
60
+
61
+ if (depth === 0) {
62
+ const segment = arrayBody.slice(lastIdx, i + 1);
63
+ re.lastIndex = 0;
64
+ const m = re.exec(segment);
65
+ if (m) {
66
+ keys.push(m[1]);
67
+ }
68
+ if (ch === ",") {
69
+ lastIdx = i + 1;
70
+ }
71
+ }
72
+ }
73
+
74
+ if (lastIdx < arrayBody.length) {
75
+ const segment = arrayBody.slice(lastIdx);
76
+ re.lastIndex = 0;
77
+ const m = re.exec(segment);
78
+ if (m) {
79
+ keys.push(m[1]);
80
+ }
81
+ }
82
+
83
+ return [...new Set(keys)];
84
+ }
85
+
86
+ /**
87
+ * 配列内の文字列値を全抽出する。
88
+ * `$uses = array('Content', 'Title')` → ['Content', 'Title']
89
+ */
90
+ export function extractQuotedStrings(arrayBody) {
91
+ const strings = [];
92
+ const re = /['"]([A-Za-z0-9_\/.]+)['"]/g;
93
+ let m;
94
+ while ((m = re.exec(arrayBody)) !== null) {
95
+ strings.push(m[1]);
96
+ }
97
+ return strings;
98
+ }
99
+
100
+ /**
101
+ * CamelCase → snake_case 変換。
102
+ */
103
+ export function camelToSnake(name) {
104
+ return name
105
+ .replace(/([A-Z])/g, (_, ch, idx) =>
106
+ idx === 0 ? ch.toLowerCase() : `_${ch.toLowerCase()}`,
107
+ )
108
+ .replace(/__+/g, "_");
109
+ }
110
+
111
+ /**
112
+ * 簡易英語複数形変換。CakePHP 2.x 規約準拠。
113
+ */
114
+ export function pluralize(word) {
115
+ if (!word) return word;
116
+ const lower = word.toLowerCase();
117
+
118
+ const irregulars = {
119
+ person: "people",
120
+ man: "men",
121
+ child: "children",
122
+ sex: "sexes",
123
+ move: "moves",
124
+ goose: "geese",
125
+ mouse: "mice",
126
+ };
127
+ if (irregulars[lower]) {
128
+ return word[0] === word[0].toUpperCase()
129
+ ? irregulars[lower][0].toUpperCase() + irregulars[lower].slice(1)
130
+ : irregulars[lower];
131
+ }
132
+
133
+ if (/(?:s|x|z|ch|sh)$/i.test(word)) return `${word}es`;
134
+ if (/[^aeiou]y$/i.test(word)) return word.slice(0, -1) + "ies";
135
+ if (/f$/i.test(word)) return word.slice(0, -1) + "ves";
136
+ if (/fe$/i.test(word)) return word.slice(0, -2) + "ves";
137
+ return `${word}s`;
138
+ }
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sdd-forge/analyzers/scan.js
4
+ *
5
+ * 全解析器を実行し、結合結果を .sdd-forge/output/analysis.json へ出力する。
6
+ * オプション: --only controllers|models|shells|routes, --stdout
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { analyzeControllers } from "./analyze-controllers.js";
12
+ import { analyzeModels } from "./analyze-models.js";
13
+ import { analyzeShells } from "./analyze-shells.js";
14
+ import { analyzeRoutes } from "./analyze-routes.js";
15
+ import { analyzeExtras } from "./analyze-extras.js";
16
+ import { repoRoot, sourceRoot, parseArgs } from "../lib/cli.js";
17
+
18
+ function printHelp() {
19
+ console.log(
20
+ [
21
+ "Usage: node sdd-forge/analyzers/scan.js [options]",
22
+ "",
23
+ "Options:",
24
+ " --only <type> controllers|models|shells|routes|extras のいずれか",
25
+ " --stdout 結果を stdout に出力(ファイル書き込みしない)",
26
+ " -h, --help このヘルプを表示",
27
+ ].join("\n"),
28
+ );
29
+ }
30
+
31
+ const VALID_ONLY = new Set(["controllers", "models", "shells", "routes", "extras"]);
32
+
33
+ function main() {
34
+ const cli = parseArgs(process.argv.slice(2), {
35
+ flags: ["--stdout"],
36
+ options: ["--only"],
37
+ defaults: { only: "", stdout: false },
38
+ });
39
+ if (cli.help) {
40
+ printHelp();
41
+ return;
42
+ }
43
+
44
+ if (cli.only && !VALID_ONLY.has(cli.only)) {
45
+ throw new Error(
46
+ `--only must be one of: ${[...VALID_ONLY].join(", ")}`,
47
+ );
48
+ }
49
+
50
+ const root = repoRoot(import.meta.url);
51
+ const appDir = path.join(sourceRoot(), "app");
52
+
53
+ if (!fs.existsSync(appDir)) {
54
+ throw new Error(`app/ directory not found: ${appDir}`);
55
+ }
56
+
57
+ const result = { analyzedAt: new Date().toISOString() };
58
+ const run = !cli.only;
59
+
60
+ if (run || cli.only === "controllers") {
61
+ console.error("[analyze] controllers ...");
62
+ result.controllers = analyzeControllers(appDir);
63
+ console.error(
64
+ `[analyze] controllers: ${result.controllers.summary.total} files, ${result.controllers.summary.totalActions} actions`,
65
+ );
66
+ }
67
+
68
+ if (run || cli.only === "models") {
69
+ console.error("[analyze] models ...");
70
+ result.models = analyzeModels(appDir);
71
+ console.error(
72
+ `[analyze] models: ${result.models.summary.total} files (fe=${result.models.summary.feModels}, logic=${result.models.summary.logicModels})`,
73
+ );
74
+ }
75
+
76
+ if (run || cli.only === "shells") {
77
+ console.error("[analyze] shells ...");
78
+ result.shells = analyzeShells(appDir);
79
+ console.error(
80
+ `[analyze] shells: ${result.shells.summary.total} files`,
81
+ );
82
+ }
83
+
84
+ if (run || cli.only === "routes") {
85
+ console.error("[analyze] routes ...");
86
+ result.routes = analyzeRoutes(appDir);
87
+ console.error(
88
+ `[analyze] routes: ${result.routes.summary.total} routes`,
89
+ );
90
+ }
91
+
92
+ if (run || cli.only === "extras") {
93
+ console.error("[analyze] extras ...");
94
+ result.extras = analyzeExtras(appDir);
95
+ const extrasKeys = Object.keys(result.extras);
96
+ console.error(
97
+ `[analyze] extras: ${extrasKeys.length} categories (${extrasKeys.join(", ")})`,
98
+ );
99
+ }
100
+
101
+ const json = JSON.stringify(result, null, 2);
102
+
103
+ if (cli.stdout) {
104
+ process.stdout.write(json + "\n");
105
+ } else {
106
+ const outputDir = path.join(root, ".sdd-forge", "output");
107
+ if (!fs.existsSync(outputDir)) {
108
+ fs.mkdirSync(outputDir, { recursive: true });
109
+ }
110
+ const outputPath = path.join(outputDir, "analysis.json");
111
+ fs.writeFileSync(outputPath, json + "\n");
112
+ console.error(`[analyze] output: ${path.relative(root, outputPath)}`);
113
+ }
114
+ }
115
+
116
+ main();
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bin/sdd-forge.js
4
+ *
5
+ * sdd-forge のメイン CLI エントリポイント。
6
+ * サブコマンドを対応スクリプトへ振り分ける。
7
+ *
8
+ * Usage:
9
+ * sdd-forge <subcommand> [options]
10
+ * sdd-forge --project <name> <subcommand> [options]
11
+ * sdd-forge help
12
+ */
13
+
14
+ import { fileURLToPath } from "url";
15
+ import path from "path";
16
+
17
+ const PKG_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
18
+
19
+ /** サブコマンド → スクリプト相対パス */
20
+ const SCRIPTS = {
21
+ help: "help.js",
22
+ add: "projects/add.js",
23
+ default: "projects/setdefault.js",
24
+ scan: "analyzers/scan.js",
25
+ "scan:ctrl": "analyzers/scan.js",
26
+ "scan:model": "analyzers/scan.js",
27
+ "scan:shell": "analyzers/scan.js",
28
+ "scan:route": "analyzers/scan.js",
29
+ "scan:extra": "analyzers/scan.js",
30
+ init: "engine/init.js",
31
+ populate: "engine/populate.js",
32
+ tfill: "engine/tfill.js",
33
+ readme: "engine/readme.js",
34
+ spec: "spec/spec.js",
35
+ gate: "spec/gate.js",
36
+ forge: "forge/forge.js",
37
+ flow: "flow/flow.js",
38
+ };
39
+
40
+ /** サブコマンドに注入する追加引数 */
41
+ const INJECT = {
42
+ "scan:ctrl": ["--only", "controllers"],
43
+ "scan:model": ["--only", "models"],
44
+ "scan:shell": ["--only", "shells"],
45
+ "scan:route": ["--only", "routes"],
46
+ "scan:extra": ["--only", "extras"],
47
+ };
48
+
49
+ /** プロジェクトコンテキスト解決が不要なコマンド */
50
+ const PROJECT_MGMT = new Set(["add", "default", "help"]);
51
+
52
+ // --project フラグを argv から抽出
53
+ const rawArgs = process.argv.slice(2);
54
+ let projectName;
55
+ let filteredArgs = rawArgs;
56
+
57
+ const projIdx = rawArgs.indexOf("--project");
58
+ if (projIdx !== -1) {
59
+ projectName = rawArgs[projIdx + 1];
60
+ filteredArgs = [
61
+ ...rawArgs.slice(0, projIdx),
62
+ ...rawArgs.slice(projIdx + 2),
63
+ ];
64
+ }
65
+
66
+ const [subCmd, ...rest] = filteredArgs;
67
+
68
+ // ヘルプ(引数なし / -h / --help)
69
+ if (!subCmd || subCmd === "-h" || subCmd === "--help") {
70
+ const helpPath = path.join(PKG_DIR, "help.js");
71
+ process.argv = [process.argv[0], helpPath];
72
+ await import(helpPath);
73
+ process.exit(0);
74
+ }
75
+
76
+ // プロジェクトコンテキストの解決(管理コマンド以外)
77
+ if (!PROJECT_MGMT.has(subCmd)) {
78
+ const { resolveProject, workRootFor } = await import(
79
+ path.join(PKG_DIR, "projects/projects.js")
80
+ );
81
+ try {
82
+ const project = resolveProject(projectName);
83
+ if (project) {
84
+ process.env.SDD_SOURCE_ROOT = project.path;
85
+ process.env.SDD_WORK_ROOT = workRootFor(project.name);
86
+ }
87
+ } catch (err) {
88
+ console.error(`Error: ${err.message}`);
89
+ process.exit(1);
90
+ }
91
+ }
92
+
93
+ // scan:all: 全解析 → populate を順に実行
94
+ if (subCmd === "scan:all") {
95
+ const scanPath = path.join(PKG_DIR, "analyzers/scan.js");
96
+ const populatePath = path.join(PKG_DIR, "engine/populate.js");
97
+
98
+ process.argv = [process.argv[0], scanPath, ...rest];
99
+ await import(scanPath);
100
+
101
+ process.argv = [process.argv[0], populatePath];
102
+ await import(populatePath);
103
+ process.exit(0);
104
+ }
105
+
106
+ const scriptRelPath = SCRIPTS[subCmd];
107
+ if (!scriptRelPath) {
108
+ console.error(`sdd-forge: unknown command '${subCmd}'`);
109
+ console.error("Run: sdd-forge help");
110
+ process.exit(1);
111
+ }
112
+
113
+ const scriptPath = path.join(PKG_DIR, scriptRelPath);
114
+ const inject = INJECT[subCmd] || [];
115
+ process.argv = [process.argv[0], scriptPath, ...inject, ...rest];
116
+
117
+ await import(scriptPath);
@@ -0,0 +1,72 @@
1
+ /**
2
+ * tools/engine/directive-parser.js
3
+ *
4
+ * テンプレート内の @data-fill / @text-fill ディレクティブを抽出する。
5
+ *
6
+ * ディレクティブ構文:
7
+ * <!-- @data-fill: <renderer>(<category>, labels=<label1>|<label2>|...) -->
8
+ * <!-- @text-fill: <prompt text> -->
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} DataFillDirective
13
+ * @property {"data-fill"} type
14
+ * @property {string} renderer - table | kv | mermaid-er | bool-matrix
15
+ * @property {string} category - 汎用カテゴリ名
16
+ * @property {string[]} labels - テーブルヘッダー表示名
17
+ * @property {string} raw - ディレクティブ行の全文
18
+ * @property {number} line - 行番号 (0-based)
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} TextFillDirective
23
+ * @property {"text-fill"} type
24
+ * @property {string} prompt - LLM に渡すプロンプト
25
+ * @property {string} raw - ディレクティブ行の全文
26
+ * @property {number} line - 行番号 (0-based)
27
+ */
28
+
29
+ const DATA_FILL_RE = /^<!--\s*@data-fill:\s*([\w-]+)\(([^,)]+)(?:,\s*labels=([^)]+))?\)\s*-->$/;
30
+ const TEXT_FILL_RE = /^<!--\s*@text-fill:\s*(.+?)\s*-->$/;
31
+
32
+ /**
33
+ * テンプレートテキストからディレクティブを抽出する。
34
+ *
35
+ * @param {string} text - テンプレート全文
36
+ * @returns {Array<DataFillDirective|TextFillDirective>}
37
+ */
38
+ export function parseDirectives(text) {
39
+ const lines = text.split("\n");
40
+ const directives = [];
41
+
42
+ for (let i = 0; i < lines.length; i++) {
43
+ const trimmed = lines[i].trim();
44
+
45
+ const dataMatch = trimmed.match(DATA_FILL_RE);
46
+ if (dataMatch) {
47
+ const labels = dataMatch[3] ? dataMatch[3].split("|").map((l) => l.trim()) : [];
48
+ directives.push({
49
+ type: "data-fill",
50
+ renderer: dataMatch[1],
51
+ category: dataMatch[2].trim(),
52
+ labels,
53
+ raw: lines[i],
54
+ line: i,
55
+ });
56
+ continue;
57
+ }
58
+
59
+ const textMatch = trimmed.match(TEXT_FILL_RE);
60
+ if (textMatch) {
61
+ directives.push({
62
+ type: "text-fill",
63
+ prompt: textMatch[1],
64
+ raw: lines[i],
65
+ line: i,
66
+ });
67
+ continue;
68
+ }
69
+ }
70
+
71
+ return directives;
72
+ }