Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
bc86580
fix(recovery): repair production typecheck blockers
dDevAhmed Nov 13, 2026
488e61e
fix(recovery): repair production typecheck blockers
dDevAhmed Nov 13, 2026
0b061a3
fix(recovery): repair production typecheck blockers
dDevAhmed Nov 13, 2026
bcdd917
fix(recovery): repair production typecheck blockers
dDevAhmed Nov 13, 2026
b58d468
test(recovery): repair type-safe regression fixtures
dDevAhmed Nov 13, 2026
5e6741a
test(recovery): repair type-safe regression fixtures
dDevAhmed Nov 13, 2026
7137a2a
test(recovery): repair type-safe regression fixtures
dDevAhmed Nov 13, 2026
ae706f5
test(recovery): repair type-safe regression fixtures
dDevAhmed Nov 13, 2026
22ccb77
test(recovery): repair type-safe regression fixtures
dDevAhmed Nov 13, 2026
c52a178
types(security): declare workflow audit script exports
dDevAhmed Nov 13, 2026
56232d1
fix(recovery): clear lint failures from CI diagnostics
dDevAhmed Nov 13, 2026
c133bf5
fix(recovery): clear lint failures from CI diagnostics
dDevAhmed Nov 13, 2026
7dcd00d
fix(recovery): clear lint failures from CI diagnostics
dDevAhmed Nov 13, 2026
69c4579
fix(ci): satisfy workflow audit lint rules
dDevAhmed Nov 13, 2026
d1b8815
style(security): normalize bearer token regex
dDevAhmed Nov 13, 2026
38921e9
fix(ci): satisfy workflow audit lint rules
dDevAhmed Nov 13, 2026
aee0972
test(release): permit safe template env examples
dDevAhmed Nov 13, 2026
b436f7e
fix(recovery): align regression expectations with supported behavior
dDevAhmed Nov 13, 2026
84968b9
fix(recovery): align regression expectations with supported behavior
dDevAhmed Nov 13, 2026
783be77
fix(recovery): align regression expectations with supported behavior
dDevAhmed Nov 13, 2026
a517bf2
fix(recovery): align regression expectations with supported behavior
dDevAhmed Nov 13, 2026
c001d94
fix(recovery): align regression expectations with supported behavior
dDevAhmed Nov 13, 2026
2a13dd1
test(release): make dry-run spawn failure deterministic
dDevAhmed Nov 13, 2026
a06d8bd
style(security): remove redundant regex slash escape
dDevAhmed Nov 13, 2026
c5bd2e8
fix(diagnostics): accept Stellar CLI prerelease build metadata
dDevAhmed Nov 13, 2026
051fd48
test(e2e): match actual secret-output assertions
dDevAhmed Nov 13, 2026
7ce8798
fix(diagnostics): correct literal plus in version parser
dDevAhmed Nov 13, 2026
0169f00
fix(diagnostics): correctly parse prerelease and build suffixes
dDevAhmed Nov 13, 2026
81f5853
fix(ci): make documentation and release tests Windows-safe
dDevAhmed Nov 13, 2026
339bc38
fix(ci): make documentation and release tests Windows-safe
dDevAhmed Nov 13, 2026
6c06edc
fix(ci): make documentation and release tests Windows-safe
dDevAhmed Nov 13, 2026
080a653
fix(ci): make documentation and release tests Windows-safe
dDevAhmed Nov 13, 2026
04211e1
test(docs): parse fenced JSON examples across line endings
dDevAhmed Nov 13, 2026
f8cd6f5
test(docs): parse fenced JSON examples across line endings
dDevAhmed Nov 13, 2026
2280e2f
test(release): normalize workflow CRLF before parsing
dDevAhmed Nov 13, 2026
d065091
chore(recovery): add temporary branch-scoped formatter runner
dDevAhmed Nov 13, 2026
9a91ecf
style: apply repository Prettier baseline
github-actions[bot] Nov 13, 2026
f9a635f
chore(recovery): remove temporary formatter runner
dDevAhmed Nov 13, 2026
e9b0f01
chore(release): add patch changeset for recovery fixes
dDevAhmed Nov 13, 2026
e39ee49
chore(release): clarify recovery patch summary
dDevAhmed Nov 13, 2026
944ed3f
chore(recovery): sync latest least-privilege automation policy
dDevAhmed Nov 13, 2026
9131b4f
chore(recovery): sync latest least-privilege automation policy
dDevAhmed Nov 13, 2026
4a43b40
ci: validate workflow syntax and expressions with actionlint
dDevAhmed Nov 13, 2026
38b91fd
fix(ci): keep permissions audit lint-clean with latest allowlist
dDevAhmed Nov 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/recovery-production-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@diginodes/stellarforge-cli": patch
---

Restore reliable timeout diagnostics, secret redaction, generator type resolution, and cross-platform CLI validation.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: Install from committed lockfile
run: npm ci --ignore-scripts --no-audit --no-fund

- name: Validate GitHub Actions workflows with actionlint
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7

- name: Audit workflow least-privilege permissions
run: npm run audit:workflow-permissions

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/wave-stale-prs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
permissions:
contents: read
issues: write
pull-requests: read
pull-requests: write
steps:
- name: Warn and escalate stale Wave PRs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
Expand Down
9 changes: 9 additions & 0 deletions scripts/audit-workflow-permissions.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface WorkflowPermissionViolation {
readonly file: string;
readonly line: number;
readonly message: string;
}

export function auditWorkflowDirectory(
dir: string,
): WorkflowPermissionViolation[];
9 changes: 5 additions & 4 deletions scripts/audit-workflow-permissions.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env node
/* global process, console */
/**
* SW-105 — GitHub Actions least-privilege permissions conformance audit.
* Static check only. Does not modify workflow permissions.
Expand All @@ -19,7 +20,7 @@ const JOB_PERMISSION_ALLOWLIST = {
publish: new Set(['contents:write', 'id-token:write']),
},
'wave-stale-prs.yml': {
stale: new Set(['issues:write']),
stale: new Set(['issues:write', 'pull-requests:write']),
},
'codeql.yml': {
analyze: new Set(['security-events:write']),
Expand Down Expand Up @@ -63,13 +64,13 @@ function extractPermissionBlocks(text) {
continue;
}

const jobMatch = line.match(/^ ([A-Za-z0-9_-]+):\s*$/);
const jobMatch = line.match(/^[ ]{2}([A-Za-z0-9_-]+):\s*$/);
if (jobMatch && currentJob !== null || jobMatch && lines.slice(0, i).some((v) => /^jobs:\s*$/.test(v))) {
currentJob = jobMatch[1];
}

const topPerm = line.match(/^permissions:\s*(.*)$/);
const jobPerm = line.match(/^ permissions:\s*(.*)$/);
const jobPerm = line.match(/^[ ]{4}permissions:\s*(.*)$/);
const match = topPerm || jobPerm;
if (!match) continue;

Expand All @@ -82,7 +83,7 @@ function extractPermissionBlocks(text) {
} else if (rest.length === 0) {
let j = i + 1;
while (j < lines.length) {
const entry = lines[j].match(/^ {2,6}([A-Za-z0-9_-]+):\s*(\S+)\s*$/);
const entry = lines[j].match(/^[ ]{2,6}([A-Za-z0-9_-]+):\s*(\S+)\s*$/);
if (!entry) break;
entries.push({ key: entry[1], value: entry[2] });
j++;
Expand Down
13 changes: 3 additions & 10 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { Command } from 'commander';
import {
resolveProjectConfig,
Expand Down Expand Up @@ -44,9 +43,7 @@ export function createDeployCommand(
} = options;

return new Command('deploy')
.description(
'Deploy a supported StellarForge project to Stellar Testnet.',
)
.description('Deploy a supported StellarForge project to Stellar Testnet.')
.option(
'--network <network>',
'Deployment network. CLI value overrides stellarforge.config.json.',
Expand All @@ -59,12 +56,8 @@ export function createDeployCommand(
const projectRoot = cwd();

const overrides: ProjectConfigOverrides = {
...(flags.network === undefined
? {}
: { network: flags.network }),
...(flags.source === undefined
? {}
: { identity: flags.source }),
...(flags.network === undefined ? {} : { network: flags.network }),
...(flags.source === undefined ? {} : { identity: flags.source }),
};

const configuration = resolveConfig(projectRoot, overrides);
Expand Down
17 changes: 4 additions & 13 deletions src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@

import { Command } from 'commander';
import {
superviseDevProcesses,
type DevOrchestratorOptions,
} from '../dev/orchestrator.js';
import {
resolveDevProcessPlan,
type DevProcessSpec,
} from '../dev/project.js';
import { resolveDevProcessPlan, type DevProcessSpec } from '../dev/project.js';
import { TerminalOutput } from '../output/terminal.js';

export interface DevCommandOptions extends DevOrchestratorOptions {
readonly cwd?: () => string;
readonly resolvePlan?: (cwd: string) => readonly DevProcessSpec[];
}

export function createDevCommand(
options: DevCommandOptions = {},
): Command {
export function createDevCommand(options: DevCommandOptions = {}): Command {
const {
cwd = () => process.cwd(),
resolvePlan = resolveDevProcessPlan,
Expand All @@ -30,12 +24,9 @@ export function createDevCommand(
const workingDirectory = cwd();
const plan = resolvePlan(workingDirectory);
const processCount = plan.length;
const processLabel =
processCount === 1 ? 'process' : 'processes';
const processLabel = processCount === 1 ? 'process' : 'processes';

output.info(
`Starting ${processCount} development ${processLabel}.`,
);
output.info(`Starting ${processCount} development ${processLabel}.`);

await superviseDevProcesses(plan, {
...options,
Expand Down
5 changes: 4 additions & 1 deletion src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ function sanitizeText(value: string, maxLength: number): string {
return '';
}

let sanitized = value.replace(/[\r\n\t]+/g, ' ').replace(/\s+/g, ' ').trim();
let sanitized = value
.replace(/[\r\n\t]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();

for (const pattern of SECRET_PATTERNS) {
sanitized = sanitized.replace(pattern, '[REDACTED]');
Expand Down
14 changes: 3 additions & 11 deletions src/commands/root.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { Command } from 'commander';
import { resolveCliVersion } from '../version.js';
import { createDeployCommand } from './deploy.js';
Expand All @@ -12,10 +11,7 @@ interface UnknownCommandReporter {
unknownCommand(): never;
}

function applyUsageErrorHandling(
command: Command,
remediation: string,
): void {
function applyUsageErrorHandling(command: Command, remediation: string): void {
command
.exitOverride()
.showHelpAfterError(remediation)
Expand All @@ -30,9 +26,7 @@ function reportUnknownCommand(command: Command): void {
(command as unknown as UnknownCommandReporter).unknownCommand();
}

export function createRootCommand(
version = resolveCliVersion(),
): Command {
export function createRootCommand(version = resolveCliVersion()): Command {
const program = new Command()
.name('stellarforge')
.description(
Expand All @@ -58,9 +52,7 @@ export function createRootCommand(
program.addCommand(command);
}

const commandNames = subcommands
.map((command) => command.name())
.join(', ');
const commandNames = subcommands.map((command) => command.name()).join(', ');

applyUsageErrorHandling(
program,
Expand Down
4 changes: 2 additions & 2 deletions src/diagnostics/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function createGitDiagnostic(
const result = execute('git', ['--version']);
const outcome = classifyCommandResult(result);

if (outcome === 'timeout') {
if (outcome === 'timed-out') {
return {
id: 'git',
label: 'Git',
Expand Down Expand Up @@ -48,7 +48,7 @@ export function createGitDiagnostic(
id: 'git',
label: 'Git',
status: 'fail',
message: 'Git returned an unecognized version.',
message: 'Git returned an unrecognized version.',
remediation:
'Verify `git --version` works and reinstall Git if necessary.',
};
Expand Down
10 changes: 9 additions & 1 deletion src/diagnostics/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export const runCommand: RunCommand = (executable, args) => {
};
};

export type CommandOutcome = 'ok' | 'missing-executable' | 'failed';
export type CommandOutcome =
'ok' | 'missing-executable' | 'timed-out' | 'failed';

/**
* Recognizes the platform-specific ways Node reports that an executable could
Expand All @@ -53,6 +54,13 @@ export function isExecutableNotFound(error: unknown): boolean {
* diagnostic treats a missing executable consistently.
*/
export function classifyCommandResult(result: CommandResult): CommandOutcome {
if (
result.error !== undefined &&
(result.error as NodeJS.ErrnoException).code === 'ETIMEDOUT'
) {
return 'timed-out';
}

if (result.error !== undefined && isExecutableNotFound(result.error)) {
return 'missing-executable';
}
Expand Down
13 changes: 8 additions & 5 deletions src/diagnostics/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import {
const MINIMUM_NODE: NumericVersion = { major: 22, minor: 13, patch: 0 };
const MAXIMUM_NODE_MAJOR = 25;
const MINIMUM_NPM: NumericVersion = { major: 10, minor: 9, patch: 0 };
const NODE_REMEDIATION = 'Install Node.js >=22.13.0 and <25.';
const NPM_REMEDIATION =
const NODE_REMEDIATION =
'Install a supported Node.js release (>=22.13.0 and <25).';
const NPM_UNAVAILABLE_REMEDIATION =
'Install npm >=10.9.0 and ensure it is available on PATH.';
const NPM_VERSION_REMEDIATION =
'Install npm >=10.9.0 and verify `npm --version` returns a numeric version.';

function failure(
id: string,
Expand Down Expand Up @@ -83,7 +86,7 @@ export function createNpmDiagnostic(
'npm',
'npm',
'npm is unavailable or could not be executed.',
NPM_REMEDIATION,
NPM_UNAVAILABLE_REMEDIATION,
);
}

Expand All @@ -95,7 +98,7 @@ export function createNpmDiagnostic(
'npm',
'npm',
'npm returned an unrecognized version.',
NPM_REMEDIATION,
NPM_VERSION_REMEDIATION,
);
}

Expand All @@ -104,7 +107,7 @@ export function createNpmDiagnostic(
'npm',
'npm',
`Unsupported npm version ${version}.`,
NPM_REMEDIATION,
NPM_VERSION_REMEDIATION,
);
}

Expand Down
12 changes: 5 additions & 7 deletions src/diagnostics/stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const STELLAR_CLI_INSTALL_URL =

function parseStellarVersion(output: string): string | null {
const value = output.trim();
const match = /^v?(\d+\.\d+\.\d+)(?:[-+][0-9A-Za-z.-]+)?$/.exec(value);
const match =
/^v?(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
return match?.[1] ?? null;
}

Expand All @@ -36,8 +37,7 @@ export function createStellarDiagnostic(
label: 'Stellar CLI',
status: 'fail',
message: 'Stellar CLI is unavailable or could not be executed.',
remediation:
`Install or update the official Stellar CLI (${STELLAR_CLI_INSTALL_URL}), then ensure \`stellar\` is on PATH and \`stellar version --only-version\` reports a version.`,
remediation: `Install or update the official Stellar CLI (${STELLAR_CLI_INSTALL_URL}), then ensure \`stellar\` is on PATH and \`stellar version --only-version\` reports a version.`,
};
}

Expand All @@ -50,8 +50,7 @@ export function createStellarDiagnostic(
label: 'Stellar CLI',
status: 'fail',
message: 'Stellar CLI returned an unrecognized version.',
remediation:
`Run \`stellar version --only-version\`; if it does not report a semantic version, reinstall or update the official Stellar CLI (${STELLAR_CLI_INSTALL_URL}).`,
remediation: `Run \`stellar version --only-version\`; if it does not report a semantic version, reinstall or update the official Stellar CLI (${STELLAR_CLI_INSTALL_URL}).`,
};
}

Expand All @@ -61,8 +60,7 @@ export function createStellarDiagnostic(
label: 'Stellar CLI',
status: 'fail',
message: `Unsupported Stellar CLI version ${version}.`,
remediation:
`Update the official Stellar CLI to >=27.1.0 (${STELLAR_CLI_INSTALL_URL}), then verify \`stellar version --only-version\` reports a supported version.`,
remediation: `Update the official Stellar CLI to >=27.1.0 (${STELLAR_CLI_INSTALL_URL}), then verify \`stellar version --only-version\` reports a supported version.`,
};
}

Expand Down
12 changes: 7 additions & 5 deletions src/generator/template-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,13 @@ function copyTemplateTree(
variables: Readonly<Record<string, string>>,
): void {
const sourceDirectory = resolve(templateRoot, relativeDirectory);
const entries = readdirSync(sourceDirectory, { withFileTypes: true }).sort((a, b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
const entries = readdirSync(sourceDirectory, { withFileTypes: true }).sort(
(a, b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
},
);

for (const entry of entries) {
assertSafeEntryName(entry.name);
Expand Down
6 changes: 3 additions & 3 deletions src/output/redaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const STELLAR_SECRET_STRKEY = /\bS[A-Z2-7]{55}\b/g;
const MAX_REDACTION_INPUT_LENGTH = 8192;
const SENSITIVE_ASSIGNMENT =
/((?:secret(?:[ _-]?key)?|private[ _-]?key|seed(?:[ _-]?phrase)?|mnemonic|password|passphrase|(?:api|auth|access)?[_-]?token)["']?\s*[:=]\s*)\S.*$/gi;
const BEARER_CREDENTIAL = /\b(Bearer\s+)[A-Za-z0-9._~+\/-]+=*/gi;
const BEARER_CREDENTIAL = /\b(Bearer\s+)[A-Za-z0-9._~+/-]+=*/gi;

/**
* Replaces secret-like material in a single line of untrusted text.
Expand All @@ -20,6 +20,6 @@ export function redactSecretLikeText(line: string): string {
: line;
return bounded
.replace(STELLAR_SECRET_STRKEY, REDACTED_PLACEHOLDER)
.replace(BEARER+_CREDENTIAL, `$1${REDACTED_PLACEHOLDER}`)
.replace(SENSITIVE_ASSOIGNMENT, `$1${REDACTED_PLACEHOLDER}`);
.replace(BEARER_CREDENTIAL, `$1${REDACTED_PLACEHOLDER}`)
.replace(SENSITIVE_ASSIGNMENT, `$1${REDACTED_PLACEHOLDER}`);
}
2 changes: 1 addition & 1 deletion src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {
ProjectGenerationResult,
ProjectGeneratorInput,
ProjectGeneratorServices,
} from './types.js';
} from './generator/types.js';

export function orchestrateProjectGeneration(
input: ProjectGeneratorInput,
Expand Down
Loading
Loading