sdd-cli 0.1.23 → 0.1.24

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.
@@ -1,18 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runAiExec = runAiExec;
4
- const codex_1 = require("../providers/codex");
5
4
  const prompt_1 = require("../ui/prompt");
6
5
  const errors_1 = require("../errors");
6
+ const flags_1 = require("../context/flags");
7
+ const providers_1 = require("../providers");
7
8
  async function runAiExec(promptArg) {
8
9
  const prompt = promptArg || (await (0, prompt_1.ask)("Prompt: "));
9
10
  if (!prompt) {
10
11
  (0, errors_1.printError)("SDD-1501", "Prompt is required.");
11
12
  return;
12
13
  }
13
- const result = (0, codex_1.codexExec)(prompt);
14
+ const requested = (0, flags_1.getFlags)().provider ?? "gemini";
15
+ const resolution = (0, providers_1.resolveProvider)(requested);
16
+ if (!resolution.ok) {
17
+ if (resolution.reason === "invalid") {
18
+ (0, errors_1.printError)("SDD-1506", `Invalid provider '${requested}'. ${resolution.details}`);
19
+ return;
20
+ }
21
+ (0, errors_1.printError)("SDD-1504", resolution.details);
22
+ return;
23
+ }
24
+ const result = resolution.provider.exec(prompt);
14
25
  if (!result.ok) {
15
- (0, errors_1.printError)("SDD-1502", `Codex error: ${result.error}`);
26
+ (0, errors_1.printError)("SDD-1505", `${resolution.provider.label} error: ${result.error}`);
16
27
  return;
17
28
  }
18
29
  console.log(result.output);
@@ -1,13 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runAiStatus = runAiStatus;
4
- const codex_1 = require("../providers/codex");
4
+ const flags_1 = require("../context/flags");
5
+ const providers_1 = require("../providers");
5
6
  const errors_1 = require("../errors");
6
7
  function runAiStatus() {
7
- const result = (0, codex_1.codexVersion)();
8
- if (!result.ok) {
9
- (0, errors_1.printError)("SDD-1503", `Codex not available: ${result.error}`);
8
+ const requested = (0, flags_1.getFlags)().provider ?? "gemini";
9
+ const resolution = (0, providers_1.resolveProvider)(requested);
10
+ if (!resolution.ok) {
11
+ if (resolution.reason === "invalid") {
12
+ (0, errors_1.printError)("SDD-1506", `Invalid provider '${requested}'. ${resolution.details}`);
13
+ return;
14
+ }
15
+ (0, errors_1.printError)("SDD-1504", resolution.details);
16
+ for (const provider of (0, providers_1.listProviders)()) {
17
+ const status = provider.version();
18
+ console.log(`${provider.label}: ${status.ok ? status.output : `unavailable (${status.error})`}`);
19
+ }
10
20
  return;
11
21
  }
12
- console.log(`Codex available: ${result.output}`);
22
+ const activeStatus = resolution.provider.version();
23
+ console.log(`Provider selected: ${resolution.provider.id}`);
24
+ console.log(`${resolution.provider.label} available: ${activeStatus.output}`);
13
25
  }
@@ -0,0 +1,25 @@
1
+ type RepoMetadata = {
2
+ repoName: string;
3
+ description: string;
4
+ license: string;
5
+ };
6
+ export type LifecycleContext = {
7
+ goalText?: string;
8
+ intentSignals?: string[];
9
+ };
10
+ declare function tokenizeIntent(input: string): string[];
11
+ declare function deriveRepoMetadata(projectName: string, appDir: string, context?: LifecycleContext): RepoMetadata;
12
+ export declare const __internal: {
13
+ tokenizeIntent: typeof tokenizeIntent;
14
+ deriveRepoMetadata: typeof deriveRepoMetadata;
15
+ };
16
+ export type AppLifecycleResult = {
17
+ qualityPassed: boolean;
18
+ deployPrepared: boolean;
19
+ gitPrepared: boolean;
20
+ githubPublished: boolean;
21
+ summary: string[];
22
+ qualityDiagnostics: string[];
23
+ };
24
+ export declare function runAppLifecycle(projectRoot: string, projectName: string, context?: LifecycleContext): AppLifecycleResult;
25
+ export {};
@@ -0,0 +1,505 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.__internal = void 0;
7
+ exports.runAppLifecycle = runAppLifecycle;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const child_process_1 = require("child_process");
11
+ const config_1 = require("../config");
12
+ function findFileRecursive(root, predicate, maxDepth = 4) {
13
+ const walk = (current, depth) => {
14
+ if (depth > maxDepth) {
15
+ return null;
16
+ }
17
+ const entries = fs_1.default.readdirSync(current, { withFileTypes: true });
18
+ for (const entry of entries) {
19
+ const full = path_1.default.join(current, entry.name);
20
+ const rel = path_1.default.relative(root, full).replace(/\\/g, "/");
21
+ if (entry.isDirectory()) {
22
+ if ([".git", "node_modules", "dist", "build", "target", "__pycache__"].includes(entry.name)) {
23
+ continue;
24
+ }
25
+ const nested = walk(full, depth + 1);
26
+ if (nested) {
27
+ return nested;
28
+ }
29
+ }
30
+ else if (predicate(rel.toLowerCase())) {
31
+ return rel;
32
+ }
33
+ }
34
+ return null;
35
+ };
36
+ return walk(root, 0);
37
+ }
38
+ function countTestsRecursive(root, maxDepth = 8) {
39
+ const walk = (current, depth) => {
40
+ if (depth > maxDepth) {
41
+ return 0;
42
+ }
43
+ let count = 0;
44
+ const entries = fs_1.default.readdirSync(current, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ const full = path_1.default.join(current, entry.name);
47
+ const rel = path_1.default.relative(root, full).replace(/\\/g, "/").toLowerCase();
48
+ if (entry.isDirectory()) {
49
+ if ([".git", "node_modules", "dist", "build", "target", "__pycache__", ".venv", "venv"].includes(entry.name.toLowerCase())) {
50
+ continue;
51
+ }
52
+ count += walk(full, depth + 1);
53
+ continue;
54
+ }
55
+ const ext = path_1.default.extname(entry.name).toLowerCase();
56
+ if (![".js", ".jsx", ".ts", ".tsx", ".py", ".java"].includes(ext)) {
57
+ continue;
58
+ }
59
+ const raw = fs_1.default.readFileSync(full, "utf-8");
60
+ if (ext === ".py") {
61
+ count += (raw.match(/\bdef\s+test_/g) || []).length;
62
+ }
63
+ else if (ext === ".java") {
64
+ count += (raw.match(/@Test\b/g) || []).length;
65
+ }
66
+ else if (rel.includes(".test.") || rel.includes(".spec.") || rel.includes("__tests__/")) {
67
+ count += (raw.match(/\b(test|it)\s*\(/g) || []).length;
68
+ }
69
+ }
70
+ return count;
71
+ };
72
+ return walk(root, 0);
73
+ }
74
+ function run(command, args, cwd) {
75
+ let resolved = command;
76
+ if (process.platform === "win32" && command === "npm") {
77
+ resolved = "npm.cmd";
78
+ }
79
+ const useShell = process.platform === "win32" && resolved.toLowerCase().endsWith(".cmd");
80
+ const result = useShell
81
+ ? (0, child_process_1.spawnSync)([resolved, ...args].join(" "), { cwd, encoding: "utf-8", shell: true })
82
+ : (0, child_process_1.spawnSync)(resolved, args, { cwd, encoding: "utf-8", shell: false });
83
+ const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
84
+ return {
85
+ ok: result.status === 0,
86
+ command: [resolved, ...args].join(" "),
87
+ output
88
+ };
89
+ }
90
+ function hasCommand(command) {
91
+ const check = process.platform === "win32" ? run("where", [command], process.cwd()) : run("which", [command], process.cwd());
92
+ return check.ok;
93
+ }
94
+ function runIfScript(cwd, script) {
95
+ const pkgPath = path_1.default.join(cwd, "package.json");
96
+ if (!fs_1.default.existsSync(pkgPath)) {
97
+ return null;
98
+ }
99
+ try {
100
+ const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, "utf-8"));
101
+ if (!pkg.scripts || !pkg.scripts[script]) {
102
+ return null;
103
+ }
104
+ return run("npm", ["run", script], cwd);
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ function packageNeedsInstall(cwd) {
111
+ const pkgPath = path_1.default.join(cwd, "package.json");
112
+ if (!fs_1.default.existsSync(pkgPath)) {
113
+ return false;
114
+ }
115
+ try {
116
+ const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, "utf-8"));
117
+ const depCount = Object.keys(pkg.dependencies ?? {}).length + Object.keys(pkg.devDependencies ?? {}).length;
118
+ return depCount > 0;
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ }
124
+ function basicQualityCheck(appDir) {
125
+ const required = ["README.md"];
126
+ const missing = required.filter((name) => !fs_1.default.existsSync(path_1.default.join(appDir, name)));
127
+ if (missing.length > 0) {
128
+ return {
129
+ ok: false,
130
+ command: "basic-quality-check",
131
+ output: `Missing files: ${missing.join(", ")}`
132
+ };
133
+ }
134
+ return {
135
+ ok: true,
136
+ command: "basic-quality-check",
137
+ output: "Basic checks passed"
138
+ };
139
+ }
140
+ function advancedQualityCheck(appDir, context) {
141
+ const readmePath = path_1.default.join(appDir, "README.md");
142
+ const hasReadme = fs_1.default.existsSync(readmePath);
143
+ const hasLicense = fs_1.default.existsSync(path_1.default.join(appDir, "LICENSE"));
144
+ if (!hasReadme) {
145
+ return {
146
+ ok: false,
147
+ command: "advanced-quality-check",
148
+ output: "Missing README.md"
149
+ };
150
+ }
151
+ const files = fs_1.default.readdirSync(appDir);
152
+ const hasPackage = files.includes("package.json");
153
+ const hasRequirements = files.includes("requirements.txt");
154
+ if (hasPackage && hasRequirements) {
155
+ return {
156
+ ok: false,
157
+ command: "advanced-quality-check",
158
+ output: "Mixed runtime manifests detected (package.json + requirements.txt). Pick one runtime stack."
159
+ };
160
+ }
161
+ const testCount = countTestsRecursive(appDir);
162
+ if (testCount < 5) {
163
+ return {
164
+ ok: false,
165
+ command: "advanced-quality-check",
166
+ output: `Expected at least 5 tests, found ${testCount}`
167
+ };
168
+ }
169
+ const readme = fs_1.default.readFileSync(readmePath, "utf-8").toLowerCase();
170
+ const requiredSections = ["features", "test"];
171
+ const missingSections = requiredSections.filter((section) => !readme.includes(section));
172
+ if (missingSections.length > 0) {
173
+ return {
174
+ ok: false,
175
+ command: "advanced-quality-check",
176
+ output: `README missing sections: ${missingSections.join(", ")}`
177
+ };
178
+ }
179
+ if (!/\brun\b|\bstart\b|\bsetup\b/.test(readme)) {
180
+ return {
181
+ ok: false,
182
+ command: "advanced-quality-check",
183
+ output: "README missing execution/start instructions"
184
+ };
185
+ }
186
+ const schemaDoc = findFileRecursive(appDir, (rel) => rel === "schemas.md" || rel.endsWith("/schemas.md")) ??
187
+ findFileRecursive(appDir, (rel) => rel.includes("schema") && rel.endsWith(".md"));
188
+ if (!schemaDoc) {
189
+ return {
190
+ ok: false,
191
+ command: "advanced-quality-check",
192
+ output: "Missing schemas.md (or equivalent schema markdown document)"
193
+ };
194
+ }
195
+ const dummyLocalDoc = findFileRecursive(appDir, (rel) => rel.includes("dummylocal") && rel.endsWith(".md")) ??
196
+ findFileRecursive(appDir, (rel) => rel.includes("dummy-local") && rel.endsWith(".md")) ??
197
+ findFileRecursive(appDir, (rel) => rel.includes("dummy_local") && rel.endsWith(".md"));
198
+ if (!dummyLocalDoc) {
199
+ return {
200
+ ok: false,
201
+ command: "advanced-quality-check",
202
+ output: "Missing DummyLocal integration doc (expected markdown file with dummylocal/dummy-local in name)"
203
+ };
204
+ }
205
+ const regressionEvidence = findFileRecursive(appDir, (rel) => rel.includes("regression") && (rel.endsWith(".md") || rel.endsWith(".js") || rel.endsWith(".py") || rel.endsWith(".java"))) ??
206
+ (readme.includes("regression") ? "README.md" : null);
207
+ if (!regressionEvidence) {
208
+ return {
209
+ ok: false,
210
+ command: "advanced-quality-check",
211
+ output: "Missing regression testing evidence (regression doc or tests)"
212
+ };
213
+ }
214
+ const goalText = context?.goalText?.trim();
215
+ if (goalText) {
216
+ const readmeRaw = fs_1.default.readFileSync(readmePath, "utf-8");
217
+ const projectCorpus = normalizeText([
218
+ readmeRaw,
219
+ ...fs_1.default
220
+ .readdirSync(appDir)
221
+ .filter((name) => !name.startsWith("."))
222
+ .slice(0, 50)
223
+ ].join("\n"));
224
+ const intentKeywords = [
225
+ ...tokenizeIntent(goalText),
226
+ ...(context?.intentSignals ?? []).flatMap((signal) => tokenizeIntent(signal))
227
+ ];
228
+ const uniqueKeywords = [...new Set(intentKeywords)].filter((keyword) => keyword.length >= 3);
229
+ if (uniqueKeywords.length > 0) {
230
+ const matched = uniqueKeywords.filter((keyword) => {
231
+ const pattern = new RegExp(`\\b${keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
232
+ return pattern.test(projectCorpus);
233
+ });
234
+ const minimumMatches = uniqueKeywords.length >= 5 ? 3 : uniqueKeywords.length >= 3 ? 2 : 1;
235
+ if (matched.length < minimumMatches) {
236
+ const missing = uniqueKeywords.filter((keyword) => !matched.includes(keyword)).slice(0, 6);
237
+ return {
238
+ ok: false,
239
+ command: "advanced-quality-check",
240
+ output: `Intent alignment failed. Matched ${matched.length}/${uniqueKeywords.length} keywords. Missing examples: ${missing.join(", ")}`
241
+ };
242
+ }
243
+ }
244
+ }
245
+ return {
246
+ ok: true,
247
+ command: "advanced-quality-check",
248
+ output: `Advanced checks passed (${testCount} tests, license: ${hasLicense ? "yes" : "no"}, schema: ${schemaDoc}, dummy: ${dummyLocalDoc}, regression: ${regressionEvidence})`
249
+ };
250
+ }
251
+ function slugify(input) {
252
+ return input
253
+ .trim()
254
+ .toLowerCase()
255
+ .replace(/[^a-z0-9]+/g, "-")
256
+ .replace(/^-+|-+$/g, "")
257
+ .replace(/-+/g, "-");
258
+ }
259
+ function normalizeText(input) {
260
+ return input
261
+ .toLowerCase()
262
+ .normalize("NFD")
263
+ .replace(/[\u0300-\u036f]/g, "");
264
+ }
265
+ function tokenizeIntent(input) {
266
+ const stopwords = new Set([
267
+ "a",
268
+ "an",
269
+ "the",
270
+ "and",
271
+ "or",
272
+ "to",
273
+ "for",
274
+ "de",
275
+ "del",
276
+ "la",
277
+ "el",
278
+ "los",
279
+ "las",
280
+ "un",
281
+ "una",
282
+ "y",
283
+ "o",
284
+ "con",
285
+ "para",
286
+ "using",
287
+ "use",
288
+ "app",
289
+ "aplicacion",
290
+ "aplicaciones",
291
+ "create",
292
+ "crear",
293
+ "crea",
294
+ "build",
295
+ "hacer",
296
+ "haz",
297
+ "sistema",
298
+ "gestion",
299
+ "management"
300
+ ]);
301
+ const normalized = normalizeText(input);
302
+ const tokens = normalized.split(/[^a-z0-9]+/g).filter((token) => token.length >= 3 && !stopwords.has(token));
303
+ return [...new Set(tokens)].slice(0, 14);
304
+ }
305
+ function detectLicense(appDir) {
306
+ const licensePath = path_1.default.join(appDir, "LICENSE");
307
+ if (!fs_1.default.existsSync(licensePath)) {
308
+ return "MIT";
309
+ }
310
+ const raw = fs_1.default.readFileSync(licensePath, "utf-8").toUpperCase();
311
+ if (raw.includes("MIT LICENSE")) {
312
+ return "MIT";
313
+ }
314
+ if (raw.includes("APACHE LICENSE")) {
315
+ return "Apache-2.0";
316
+ }
317
+ if (raw.includes("GNU GENERAL PUBLIC LICENSE")) {
318
+ return "GPL-3.0";
319
+ }
320
+ return "MIT";
321
+ }
322
+ function deriveRepoMetadata(projectName, appDir, context) {
323
+ const readmePath = path_1.default.join(appDir, "README.md");
324
+ const rawBase = projectName
325
+ .replace(/^autopilot-/i, "")
326
+ .replace(/-\d{8}$/g, "")
327
+ .replace(/-generated-app$/g, "")
328
+ .trim();
329
+ const goalSeed = context?.goalText ? tokenizeIntent(context.goalText).slice(0, 6).join("-") : "";
330
+ const projectSeed = slugify(rawBase);
331
+ const intentSeed = slugify(goalSeed);
332
+ const base = projectSeed || intentSeed || "sdd-project";
333
+ const cleaned = base.replace(/-app$/g, "");
334
+ const repoName = `${cleaned}-app`.slice(0, 63).replace(/-+$/g, "");
335
+ let description = context?.goalText?.trim()
336
+ ? `Generated with sdd-tool: ${context.goalText.trim().slice(0, 150)}`
337
+ : `Production-ready app generated by sdd-tool for ${projectName}.`;
338
+ if ((!context?.goalText || description.length < 30) && fs_1.default.existsSync(readmePath)) {
339
+ const lines = fs_1.default
340
+ .readFileSync(readmePath, "utf-8")
341
+ .split(/\r?\n/)
342
+ .map((line) => line.trim());
343
+ const descLine = lines.find((line) => line.length > 0 && !line.startsWith("#"));
344
+ if (descLine) {
345
+ description = descLine.slice(0, 200);
346
+ }
347
+ }
348
+ return {
349
+ repoName,
350
+ description,
351
+ license: detectLicense(appDir)
352
+ };
353
+ }
354
+ exports.__internal = {
355
+ tokenizeIntent,
356
+ deriveRepoMetadata
357
+ };
358
+ function createDeployBundle(appDir) {
359
+ const deployDir = path_1.default.join(appDir, "deploy");
360
+ fs_1.default.mkdirSync(deployDir, { recursive: true });
361
+ const reportPath = path_1.default.join(deployDir, "deployment.md");
362
+ const lines = [
363
+ "# Deployment Report",
364
+ "",
365
+ `Generated at: ${new Date().toISOString()}`,
366
+ "",
367
+ "## Local Deployment",
368
+ "- App files are available under this directory.",
369
+ "- For static web apps, open `index.html` or serve folder via any static host.",
370
+ "",
371
+ "## Publish",
372
+ "- GitHub publish is attempted automatically when `gh` is authenticated."
373
+ ];
374
+ fs_1.default.writeFileSync(reportPath, `${lines.join("\n")}\n`, "utf-8");
375
+ return { ok: true, command: "write deploy/deployment.md", output: reportPath };
376
+ }
377
+ function ensureGitIgnore(appDir) {
378
+ const file = path_1.default.join(appDir, ".gitignore");
379
+ if (fs_1.default.existsSync(file)) {
380
+ return;
381
+ }
382
+ fs_1.default.writeFileSync(file, "node_modules/\ndist/\ncoverage/\n.env\n", "utf-8");
383
+ }
384
+ function ensureGitRepo(appDir) {
385
+ if (!hasCommand("git")) {
386
+ return { ok: false, command: "git", output: "git not available" };
387
+ }
388
+ ensureGitIgnore(appDir);
389
+ const init = fs_1.default.existsSync(path_1.default.join(appDir, ".git")) ? { ok: true, command: "git init", output: "existing repository" } : run("git", ["init"], appDir);
390
+ if (!init.ok) {
391
+ return init;
392
+ }
393
+ run("git", ["branch", "-M", "main"], appDir);
394
+ const nameCheck = run("git", ["config", "--get", "user.name"], appDir);
395
+ if (!nameCheck.ok || !nameCheck.output.trim()) {
396
+ run("git", ["config", "user.name", "sdd-cli-bot"], appDir);
397
+ }
398
+ const emailCheck = run("git", ["config", "--get", "user.email"], appDir);
399
+ if (!emailCheck.ok || !emailCheck.output.trim()) {
400
+ run("git", ["config", "user.email", "sdd-cli-bot@local"], appDir);
401
+ }
402
+ run("git", ["add", "."], appDir);
403
+ const commit = run("git", ["commit", "-m", "feat: generated app lifecycle output"], appDir);
404
+ if (!commit.ok && !/nothing to commit/i.test(commit.output)) {
405
+ return commit;
406
+ }
407
+ return { ok: true, command: "git init/add/commit", output: commit.output || "committed" };
408
+ }
409
+ function tryPublishGitHub(appDir, metadata) {
410
+ if (!hasCommand("gh")) {
411
+ return { ok: false, command: "gh", output: "gh CLI not available" };
412
+ }
413
+ const auth = run("gh", ["auth", "status"], appDir);
414
+ if (!auth.ok) {
415
+ return { ok: false, command: "gh auth status", output: "gh not authenticated" };
416
+ }
417
+ const remote = run("git", ["remote", "get-url", "origin"], appDir);
418
+ if (remote.ok) {
419
+ const push = run("git", ["push", "-u", "origin", "main"], appDir);
420
+ if (!push.ok) {
421
+ return { ok: false, command: push.command, output: push.output };
422
+ }
423
+ const edit = run("gh", ["repo", "edit", "--description", metadata.description, "--enable-issues=true", "--enable-wiki=false"], appDir);
424
+ return edit.ok ? push : { ok: false, command: edit.command, output: edit.output };
425
+ }
426
+ const create = run("gh", ["repo", "create", metadata.repoName, "--public", "--description", metadata.description, "--source", ".", "--remote", "origin", "--push"], appDir);
427
+ return create.ok ? create : { ok: false, command: create.command, output: create.output };
428
+ }
429
+ function runAppLifecycle(projectRoot, projectName, context) {
430
+ const appDir = path_1.default.join(projectRoot, "generated-app");
431
+ const summary = [];
432
+ const repoMetadata = deriveRepoMetadata(projectName, appDir, context);
433
+ if (process.env.SDD_DISABLE_APP_LIFECYCLE === "1") {
434
+ return {
435
+ qualityPassed: false,
436
+ deployPrepared: false,
437
+ gitPrepared: false,
438
+ githubPublished: false,
439
+ summary: ["Lifecycle disabled by SDD_DISABLE_APP_LIFECYCLE=1"],
440
+ qualityDiagnostics: ["Lifecycle disabled by SDD_DISABLE_APP_LIFECYCLE=1"]
441
+ };
442
+ }
443
+ if (!fs_1.default.existsSync(appDir)) {
444
+ return {
445
+ qualityPassed: false,
446
+ deployPrepared: false,
447
+ gitPrepared: false,
448
+ githubPublished: false,
449
+ summary: ["generated-app directory missing"],
450
+ qualityDiagnostics: ["generated-app directory missing"]
451
+ };
452
+ }
453
+ const qualitySteps = [];
454
+ const install = packageNeedsInstall(appDir) ? run("npm", ["install"], appDir) : null;
455
+ if (install)
456
+ qualitySteps.push(install);
457
+ const lint = runIfScript(appDir, "lint");
458
+ if (lint)
459
+ qualitySteps.push(lint);
460
+ const test = runIfScript(appDir, "test");
461
+ if (test)
462
+ qualitySteps.push(test);
463
+ const build = runIfScript(appDir, "build");
464
+ if (build)
465
+ qualitySteps.push(build);
466
+ qualitySteps.push(advancedQualityCheck(appDir, context));
467
+ if (qualitySteps.length === 0) {
468
+ qualitySteps.push(basicQualityCheck(appDir));
469
+ }
470
+ const qualityPassed = qualitySteps.every((step) => step.ok);
471
+ const qualityDiagnostics = qualitySteps
472
+ .filter((step) => !step.ok)
473
+ .map((step) => `${step.command}: ${step.output || "no output"}`);
474
+ qualitySteps.forEach((step) => summary.push(`${step.ok ? "OK" : "FAIL"}: ${step.command}${step.ok || !step.output ? "" : ` -> ${step.output}`}`));
475
+ const deploy = createDeployBundle(appDir);
476
+ summary.push(`${deploy.ok ? "OK" : "FAIL"}: ${deploy.command}`);
477
+ const git = ensureGitRepo(appDir);
478
+ summary.push(`${git.ok ? "OK" : "FAIL"}: ${git.command}`);
479
+ const config = (0, config_1.ensureConfig)();
480
+ const publish = !config.git.publish_enabled
481
+ ? { ok: false, command: "publish", output: "disabled by config git.publish_enabled=false" }
482
+ : !qualityPassed
483
+ ? { ok: false, command: "publish", output: "skipped because quality checks failed" }
484
+ : git.ok
485
+ ? tryPublishGitHub(appDir, repoMetadata)
486
+ : { ok: false, command: "publish", output: "skipped due to git failure" };
487
+ summary.push(`${publish.ok ? "OK" : "SKIP"}: ${publish.command} ${publish.output ? `(${publish.output})` : ""}`.trim());
488
+ const reportPath = path_1.default.join(appDir, "deploy", "lifecycle-report.md");
489
+ const reportLines = [
490
+ "# Lifecycle Report",
491
+ "",
492
+ `Generated at: ${new Date().toISOString()}`,
493
+ "",
494
+ ...summary.map((line) => `- ${line}`)
495
+ ];
496
+ fs_1.default.writeFileSync(reportPath, `${reportLines.join("\n")}\n`, "utf-8");
497
+ return {
498
+ qualityPassed,
499
+ deployPrepared: deploy.ok,
500
+ gitPrepared: git.ok,
501
+ githubPublished: publish.ok,
502
+ summary,
503
+ qualityDiagnostics
504
+ };
505
+ }