litmus-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +153 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/submit.d.ts +4 -0
- package/dist/commands/submit.d.ts.map +1 -0
- package/dist/commands/submit.js +124 -0
- package/dist/commands/submit.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api.d.ts +30 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +54 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/config.d.ts +19 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +40 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/extract.d.ts +6 -0
- package/dist/lib/extract.d.ts.map +1 -0
- package/dist/lib/extract.js +12 -0
- package/dist/lib/extract.js.map +1 -0
- package/dist/lib/tracker.d.ts +7 -0
- package/dist/lib/tracker.d.ts.map +1 -0
- package/dist/lib/tracker.js +36 -0
- package/dist/lib/tracker.js.map +1 -0
- package/dist/lib/zip.d.ts +10 -0
- package/dist/lib/zip.d.ts.map +1 -0
- package/dist/lib/zip.js +96 -0
- package/dist/lib/zip.js.map +1 -0
- package/dist/utils/detect-project.d.ts +11 -0
- package/dist/utils/detect-project.d.ts.map +1 -0
- package/dist/utils/detect-project.js +55 -0
- package/dist/utils/detect-project.js.map +1 -0
- package/dist/utils/errors.d.ts +19 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +35 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/watcher.cjs +58 -0
- package/package.json +37 -0
- package/src/commands/init.ts +171 -0
- package/src/commands/submit.ts +141 -0
- package/src/index.ts +38 -0
- package/src/lib/api.ts +93 -0
- package/src/lib/config.ts +59 -0
- package/src/lib/extract.ts +15 -0
- package/src/lib/tracker.ts +38 -0
- package/src/lib/watcher.cjs +58 -0
- package/src/lib/zip.ts +104 -0
- package/src/utils/detect-project.ts +83 -0
- package/src/utils/errors.ts +41 -0
- package/tsconfig.json +19 -0
package/dist/lib/zip.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import archiver from "archiver";
|
|
2
|
+
import { createWriteStream, readFileSync, existsSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
// Directories always excluded from submissions.
|
|
6
|
+
// NOTE: .git is intentionally NOT excluded — git history is included so the
|
|
7
|
+
// analysis backend can run parse_git_history() on the submission.
|
|
8
|
+
const ALWAYS_EXCLUDE = [
|
|
9
|
+
"node_modules",
|
|
10
|
+
"__pycache__",
|
|
11
|
+
"venv",
|
|
12
|
+
".venv",
|
|
13
|
+
"env",
|
|
14
|
+
".env",
|
|
15
|
+
"target", // Rust/Java build output
|
|
16
|
+
"build",
|
|
17
|
+
"dist",
|
|
18
|
+
".gradle",
|
|
19
|
+
".idea",
|
|
20
|
+
".DS_Store",
|
|
21
|
+
"*.pyc",
|
|
22
|
+
"*.class",
|
|
23
|
+
// Legacy injected tracking files — exclude so they don't pollute submissions
|
|
24
|
+
// from assessments downloaded before the CLI-native tracking was deployed.
|
|
25
|
+
"setup.sh",
|
|
26
|
+
"tracker.py",
|
|
27
|
+
"tracker.js",
|
|
28
|
+
"heartbeat.py",
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Create a ZIP of the project directory for submission.
|
|
32
|
+
* Respects .gitignore patterns (basic support) and excludes common large dirs.
|
|
33
|
+
* Always includes .litmus/activity.jsonl (the tracking log).
|
|
34
|
+
*
|
|
35
|
+
* @param projectDir - Root of the project to zip
|
|
36
|
+
* @returns Path to the temporary ZIP file
|
|
37
|
+
*/
|
|
38
|
+
export async function createSubmissionZip(projectDir) {
|
|
39
|
+
const tmpFile = path.join(os.tmpdir(), `litmus-submission-${Date.now()}.zip`);
|
|
40
|
+
const output = createWriteStream(tmpFile);
|
|
41
|
+
const archive = archiver("zip", { zlib: { level: 6 } });
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
output.on("close", () => resolve(tmpFile));
|
|
44
|
+
archive.on("error", reject);
|
|
45
|
+
archive.pipe(output);
|
|
46
|
+
// Compute glob patterns to ignore
|
|
47
|
+
const ignoredGlobs = buildIgnoreGlobs(projectDir);
|
|
48
|
+
// Add the project directory contents (not the directory itself)
|
|
49
|
+
archive.glob("**/*", {
|
|
50
|
+
cwd: projectDir,
|
|
51
|
+
dot: true, // Include dotfiles like .litmus/
|
|
52
|
+
ignore: ignoredGlobs,
|
|
53
|
+
});
|
|
54
|
+
archive.finalize();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function buildIgnoreGlobs(projectDir) {
|
|
58
|
+
const patterns = [];
|
|
59
|
+
// Always-excluded dirs/patterns
|
|
60
|
+
for (const excluded of ALWAYS_EXCLUDE) {
|
|
61
|
+
if (excluded.startsWith("*")) {
|
|
62
|
+
patterns.push(excluded);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
patterns.push(`${excluded}/**`);
|
|
66
|
+
patterns.push(`**/${excluded}/**`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Basic .gitignore support
|
|
70
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
71
|
+
if (existsSync(gitignorePath)) {
|
|
72
|
+
try {
|
|
73
|
+
const lines = readFileSync(gitignorePath, "utf8")
|
|
74
|
+
.split("\n")
|
|
75
|
+
.map((l) => l.trim())
|
|
76
|
+
.filter((l) => l && !l.startsWith("#"));
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
if (line.endsWith("/")) {
|
|
79
|
+
// Directory pattern
|
|
80
|
+
const dir = line.slice(0, -1);
|
|
81
|
+
patterns.push(`${dir}/**`);
|
|
82
|
+
patterns.push(`**/${dir}/**`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
patterns.push(line);
|
|
86
|
+
patterns.push(`**/${line}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Ignore .gitignore parse errors
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return patterns;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=zip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zip.js","sourceRoot":"","sources":["../../src/lib/zip.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ProjectType = "node" | "python" | "go" | "java" | "dotnet" | "rust" | "ruby" | "php" | "unknown";
|
|
2
|
+
export interface ProjectInfo {
|
|
3
|
+
type: ProjectType;
|
|
4
|
+
installCommand: string | null;
|
|
5
|
+
runHint: string | null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Detect project type and suggest an install command.
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectProject(dir: string): ProjectInfo;
|
|
11
|
+
//# sourceMappingURL=detect-project.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect-project.d.ts","sourceRoot":"","sources":["../../src/utils/detect-project.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,QAAQ,GACR,MAAM,GACN,MAAM,GACN,KAAK,GACL,SAAS,CAAA;AAEb,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CA2DtD"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Detect project type and suggest an install command.
|
|
5
|
+
*/
|
|
6
|
+
export function detectProject(dir) {
|
|
7
|
+
if (existsSync(path.join(dir, "package.json"))) {
|
|
8
|
+
const hasYarnLock = existsSync(path.join(dir, "yarn.lock"));
|
|
9
|
+
const hasPnpmLock = existsSync(path.join(dir, "pnpm-lock.yaml"));
|
|
10
|
+
const installCommand = hasPnpmLock
|
|
11
|
+
? "pnpm install"
|
|
12
|
+
: hasYarnLock
|
|
13
|
+
? "yarn"
|
|
14
|
+
: "npm install";
|
|
15
|
+
return { type: "node", installCommand, runHint: "npm start / npm test" };
|
|
16
|
+
}
|
|
17
|
+
if (existsSync(path.join(dir, "requirements.txt")) ||
|
|
18
|
+
existsSync(path.join(dir, "pyproject.toml")) ||
|
|
19
|
+
existsSync(path.join(dir, "setup.py"))) {
|
|
20
|
+
// Create a venv and install into it (replaces what the old setup.sh did).
|
|
21
|
+
// Use venv/bin/pip directly so activation isn't required at install time.
|
|
22
|
+
const pipBin = process.platform === "win32" ? "venv\\Scripts\\pip" : "venv/bin/pip";
|
|
23
|
+
const installCommand = existsSync(path.join(dir, "requirements.txt"))
|
|
24
|
+
? `python3 -m venv venv && ${pipBin} install -r requirements.txt`
|
|
25
|
+
: `python3 -m venv venv && ${pipBin} install -e .`;
|
|
26
|
+
return {
|
|
27
|
+
type: "python",
|
|
28
|
+
installCommand,
|
|
29
|
+
runHint: "source venv/bin/activate # then: python main.py / pytest",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (existsSync(path.join(dir, "go.mod"))) {
|
|
33
|
+
return { type: "go", installCommand: "go mod download", runHint: "go run . / go test ./..." };
|
|
34
|
+
}
|
|
35
|
+
if (existsSync(path.join(dir, "pom.xml"))) {
|
|
36
|
+
return { type: "java", installCommand: "mvn install", runHint: "mvn test" };
|
|
37
|
+
}
|
|
38
|
+
if (existsSync(path.join(dir, "build.gradle")) || existsSync(path.join(dir, "build.gradle.kts"))) {
|
|
39
|
+
return { type: "java", installCommand: "gradle build", runHint: "gradle test" };
|
|
40
|
+
}
|
|
41
|
+
if (existsSync(path.join(dir, "*.csproj")) || existsSync(path.join(dir, "*.sln"))) {
|
|
42
|
+
return { type: "dotnet", installCommand: "dotnet restore", runHint: "dotnet run / dotnet test" };
|
|
43
|
+
}
|
|
44
|
+
if (existsSync(path.join(dir, "Cargo.toml"))) {
|
|
45
|
+
return { type: "rust", installCommand: null, runHint: "cargo run / cargo test" };
|
|
46
|
+
}
|
|
47
|
+
if (existsSync(path.join(dir, "Gemfile"))) {
|
|
48
|
+
return { type: "ruby", installCommand: "bundle install", runHint: "ruby main.rb / rspec" };
|
|
49
|
+
}
|
|
50
|
+
if (existsSync(path.join(dir, "composer.json"))) {
|
|
51
|
+
return { type: "php", installCommand: "composer install", runHint: "php -S localhost:8000" };
|
|
52
|
+
}
|
|
53
|
+
return { type: "unknown", installCommand: null, runHint: null };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=detect-project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect-project.js","sourceRoot":"","sources":["../../src/utils/detect-project.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const FALLBACK_URL = "https://app.litmus.sh";
|
|
2
|
+
/**
|
|
3
|
+
* Print a plain-English error and exit.
|
|
4
|
+
* Always shows a fallback URL for candidates who can't resolve the issue.
|
|
5
|
+
*/
|
|
6
|
+
export declare function fatal(message: string, hint?: string): never;
|
|
7
|
+
/**
|
|
8
|
+
* Print a warning (non-fatal).
|
|
9
|
+
*/
|
|
10
|
+
export declare function warn(message: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Print a success message.
|
|
13
|
+
*/
|
|
14
|
+
export declare function success(message: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Print a plain info line.
|
|
17
|
+
*/
|
|
18
|
+
export declare function info(message: string): void;
|
|
19
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,YAAY,0BAA0B,CAAA;AAEnD;;;GAGG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAW3D;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
export const FALLBACK_URL = "https://app.litmus.sh";
|
|
3
|
+
/**
|
|
4
|
+
* Print a plain-English error and exit.
|
|
5
|
+
* Always shows a fallback URL for candidates who can't resolve the issue.
|
|
6
|
+
*/
|
|
7
|
+
export function fatal(message, hint) {
|
|
8
|
+
console.error();
|
|
9
|
+
console.error(chalk.red("✗ Error: ") + message);
|
|
10
|
+
if (hint) {
|
|
11
|
+
console.error(chalk.dim(" Hint: ") + hint);
|
|
12
|
+
}
|
|
13
|
+
console.error(chalk.dim(` Need help? Visit ${FALLBACK_URL} to manage your assessment.`));
|
|
14
|
+
console.error();
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Print a warning (non-fatal).
|
|
19
|
+
*/
|
|
20
|
+
export function warn(message) {
|
|
21
|
+
console.warn(chalk.yellow("⚠ Warning: ") + message);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Print a success message.
|
|
25
|
+
*/
|
|
26
|
+
export function success(message) {
|
|
27
|
+
console.log(chalk.green("✓ ") + message);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Print a plain info line.
|
|
31
|
+
*/
|
|
32
|
+
export function info(message) {
|
|
33
|
+
console.log(chalk.dim(" ") + message);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,MAAM,CAAC,MAAM,YAAY,GAAG,uBAAuB,CAAA;AAEnD;;;GAGG;AACH,MAAM,UAAU,KAAK,CAAC,OAAe,EAAE,IAAa;IAClD,OAAO,CAAC,KAAK,EAAE,CAAA;IACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,CAAA;IAC/C,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,CAAA;IAC7C,CAAC;IACD,OAAO,CAAC,KAAK,CACX,KAAK,CAAC,GAAG,CAAC,sBAAsB,YAAY,6BAA6B,CAAC,CAC3E,CAAA;IACD,OAAO,CAAC,KAAK,EAAE,CAAA;IACf,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,IAAI,CAAC,OAAe;IAClC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC,CAAA;AACrD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAAC,OAAe;IACrC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAA;AAC1C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,IAAI,CAAC,OAAe;IAClC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAA;AACxC,CAAC"}
|
package/dist/watcher.cjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Litmus file activity watcher.
|
|
4
|
+
* Runs as a detached background process started by `litmus init`.
|
|
5
|
+
* Usage: node watcher.cjs <projectDir> <activityLogPath>
|
|
6
|
+
*
|
|
7
|
+
* Writes newline-delimited JSON to the activity log:
|
|
8
|
+
* { "ts": "ISO-8601", "type": "add"|"change"|"unlink", "path": "relative/path" }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const chokidar = require("chokidar")
|
|
12
|
+
const fs = require("fs")
|
|
13
|
+
const path = require("path")
|
|
14
|
+
|
|
15
|
+
const [, , projectDir, activityLogPath] = process.argv
|
|
16
|
+
|
|
17
|
+
if (!projectDir || !activityLogPath) {
|
|
18
|
+
process.stderr.write("Usage: node watcher.cjs <projectDir> <activityLogPath>\n")
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Ensure the log directory exists
|
|
23
|
+
fs.mkdirSync(path.dirname(activityLogPath), { recursive: true })
|
|
24
|
+
|
|
25
|
+
// Open the log file for appending
|
|
26
|
+
const log = fs.createWriteStream(activityLogPath, { flags: "a" })
|
|
27
|
+
|
|
28
|
+
function write(type, filePath) {
|
|
29
|
+
const event = JSON.stringify({
|
|
30
|
+
ts: new Date().toISOString(),
|
|
31
|
+
type,
|
|
32
|
+
path: path.relative(projectDir, filePath),
|
|
33
|
+
})
|
|
34
|
+
log.write(event + "\n")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ignored = [
|
|
38
|
+
/(^|[/\\])\./, // dotfiles and dotdirs (including .git, .litmus)
|
|
39
|
+
/node_modules/,
|
|
40
|
+
/__pycache__/,
|
|
41
|
+
/\.pyc$/,
|
|
42
|
+
/\.class$/,
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
chokidar
|
|
46
|
+
.watch(projectDir, {
|
|
47
|
+
ignored,
|
|
48
|
+
ignoreInitial: true,
|
|
49
|
+
persistent: true,
|
|
50
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
51
|
+
})
|
|
52
|
+
.on("add", (p) => write("add", p))
|
|
53
|
+
.on("change", (p) => write("change", p))
|
|
54
|
+
.on("unlink", (p) => write("unlink", p))
|
|
55
|
+
.on("error", (err) => {
|
|
56
|
+
// Log errors but don't crash — tracking is non-critical
|
|
57
|
+
process.stderr.write(`[watcher] error: ${err}\n`)
|
|
58
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "litmus-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI tool for Litmus engineering assessments",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "elenazhao",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"litmus": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && cp src/lib/watcher.cjs dist/watcher.cjs",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"archiver": "^7.0.1",
|
|
20
|
+
"chalk": "^5.3.0",
|
|
21
|
+
"chokidar": "^3.6.0",
|
|
22
|
+
"commander": "^12.1.0",
|
|
23
|
+
"form-data": "^4.0.1",
|
|
24
|
+
"node-fetch": "^3.3.2",
|
|
25
|
+
"ora": "^8.1.1",
|
|
26
|
+
"unzipper": "^0.12.3"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/archiver": "^6.0.3",
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"@types/unzipper": "^0.10.10",
|
|
32
|
+
"typescript": "^5.4.5"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createWriteStream, unlinkSync } from "fs"
|
|
2
|
+
import { pipeline } from "stream/promises"
|
|
3
|
+
import { Readable } from "stream"
|
|
4
|
+
import path from "path"
|
|
5
|
+
import os from "os"
|
|
6
|
+
import chalk from "chalk"
|
|
7
|
+
import ora from "ora"
|
|
8
|
+
import { fetchInitMetadata, downloadZip } from "../lib/api.js"
|
|
9
|
+
import { writeConfig } from "../lib/config.js"
|
|
10
|
+
import { extractZip } from "../lib/extract.js"
|
|
11
|
+
import { startTracker } from "../lib/tracker.js"
|
|
12
|
+
import { detectProject } from "../utils/detect-project.js"
|
|
13
|
+
import { fatal, success, info, warn, FALLBACK_URL } from "../utils/errors.js"
|
|
14
|
+
import { execSync } from "child_process"
|
|
15
|
+
|
|
16
|
+
const DEFAULT_API_BASE = process.env.LITMUS_API_URL || "https://app.litmus.sh"
|
|
17
|
+
|
|
18
|
+
export async function runInit(token: string): Promise<void> {
|
|
19
|
+
const apiBase = DEFAULT_API_BASE
|
|
20
|
+
|
|
21
|
+
// Step 1: Fetch metadata
|
|
22
|
+
const spinner = ora("Connecting to Litmus...").start()
|
|
23
|
+
let metadata: Awaited<ReturnType<typeof fetchInitMetadata>>
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
metadata = await fetchInitMetadata(apiBase, token)
|
|
27
|
+
spinner.succeed("Connected")
|
|
28
|
+
} catch (err) {
|
|
29
|
+
spinner.fail("Connection failed")
|
|
30
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
31
|
+
fatal(msg, `Check your token or visit ${FALLBACK_URL} to download manually.`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for naming collision
|
|
35
|
+
const targetDir = path.resolve(process.cwd(), metadata.folderName)
|
|
36
|
+
const { existsSync } = await import("fs")
|
|
37
|
+
if (existsSync(targetDir)) {
|
|
38
|
+
fatal(
|
|
39
|
+
`Folder "${metadata.folderName}" already exists in this directory.`,
|
|
40
|
+
`Delete or rename it, then run: litmus init ${token}`
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Print what we're setting up
|
|
45
|
+
console.log()
|
|
46
|
+
console.log(chalk.bold(` ${metadata.assessmentName}`))
|
|
47
|
+
if (metadata.deadline) {
|
|
48
|
+
const deadline = new Date(metadata.deadline)
|
|
49
|
+
const hoursLeft = Math.max(
|
|
50
|
+
0,
|
|
51
|
+
Math.floor((deadline.getTime() - Date.now()) / (1000 * 60 * 60))
|
|
52
|
+
)
|
|
53
|
+
const deadlineStr = deadline.toLocaleString()
|
|
54
|
+
if (hoursLeft < 24) {
|
|
55
|
+
warn(`Deadline in ${hoursLeft} hours (${deadlineStr})`)
|
|
56
|
+
} else {
|
|
57
|
+
info(`Deadline: ${deadlineStr}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (metadata.timeLimit) {
|
|
61
|
+
info(`Time limit: ${metadata.timeLimit} minutes`)
|
|
62
|
+
}
|
|
63
|
+
console.log()
|
|
64
|
+
|
|
65
|
+
// Step 2: Download ZIP to a temp file
|
|
66
|
+
const downloadSpinner = ora("Downloading assessment...").start()
|
|
67
|
+
const tmpZip = path.join(os.tmpdir(), `litmus-${Date.now()}.zip`)
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const zipResponse = await downloadZip(apiBase, token)
|
|
71
|
+
if (!zipResponse.body) {
|
|
72
|
+
throw new Error("Empty response from server")
|
|
73
|
+
}
|
|
74
|
+
const nodeStream = Readable.fromWeb(
|
|
75
|
+
zipResponse.body as Parameters<typeof Readable.fromWeb>[0]
|
|
76
|
+
)
|
|
77
|
+
await pipeline(nodeStream, createWriteStream(tmpZip))
|
|
78
|
+
downloadSpinner.succeed("Downloaded")
|
|
79
|
+
} catch (err) {
|
|
80
|
+
downloadSpinner.fail("Download failed")
|
|
81
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
82
|
+
fatal(msg, `Visit ${FALLBACK_URL} to download manually.`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Step 3: Extract from temp file to target directory
|
|
86
|
+
const extractSpinner = ora(`Setting up ${metadata.folderName}/...`).start()
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await extractZip(tmpZip, targetDir)
|
|
90
|
+
extractSpinner.succeed(`Created ${chalk.bold(metadata.folderName)}/`)
|
|
91
|
+
} catch (err) {
|
|
92
|
+
extractSpinner.fail("Extraction failed")
|
|
93
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
94
|
+
fatal(msg, `Visit ${FALLBACK_URL} to download manually.`)
|
|
95
|
+
} finally {
|
|
96
|
+
try { unlinkSync(tmpZip) } catch { /* cleanup */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Step 4: Initialize git repo with initial commit
|
|
100
|
+
try {
|
|
101
|
+
execSync("git init", { cwd: targetDir, stdio: "pipe" })
|
|
102
|
+
execSync("git add -A", { cwd: targetDir, stdio: "pipe" })
|
|
103
|
+
execSync(
|
|
104
|
+
"git commit -m \"Assessment: initial state\"",
|
|
105
|
+
{ cwd: targetDir, stdio: "pipe" }
|
|
106
|
+
)
|
|
107
|
+
} catch {
|
|
108
|
+
// Non-critical — git may not be installed; analysis degrades gracefully
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Step 5: Write .litmus/config.json
|
|
112
|
+
await writeConfig(targetDir, {
|
|
113
|
+
assessmentId: metadata.assessmentId,
|
|
114
|
+
assessmentName: metadata.assessmentName,
|
|
115
|
+
candidateEmail: metadata.candidateEmail,
|
|
116
|
+
candidateName: metadata.candidateName,
|
|
117
|
+
token,
|
|
118
|
+
startedAt: metadata.startedAt,
|
|
119
|
+
deadline: metadata.deadline,
|
|
120
|
+
timeLimit: metadata.timeLimit,
|
|
121
|
+
apiBase,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Step 5: Detect project type and optionally install
|
|
125
|
+
const project = detectProject(targetDir)
|
|
126
|
+
if (project.installCommand) {
|
|
127
|
+
console.log()
|
|
128
|
+
const installSpinner = ora(
|
|
129
|
+
`Running ${chalk.cyan(project.installCommand)}... (this may take a minute)`
|
|
130
|
+
).start()
|
|
131
|
+
try {
|
|
132
|
+
execSync(project.installCommand, {
|
|
133
|
+
cwd: targetDir,
|
|
134
|
+
stdio: "pipe",
|
|
135
|
+
})
|
|
136
|
+
installSpinner.succeed("Dependencies installed")
|
|
137
|
+
} catch {
|
|
138
|
+
installSpinner.warn(
|
|
139
|
+
`Couldn't run "${project.installCommand}" automatically. Run it manually inside ${metadata.folderName}/.`
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Step 6: Start tracker in background
|
|
145
|
+
try {
|
|
146
|
+
startTracker(targetDir)
|
|
147
|
+
} catch {
|
|
148
|
+
// Non-critical
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Done — print next steps
|
|
152
|
+
console.log()
|
|
153
|
+
success("Assessment ready")
|
|
154
|
+
console.log()
|
|
155
|
+
console.log(chalk.bold(" Next steps:"))
|
|
156
|
+
console.log()
|
|
157
|
+
console.log(` ${chalk.cyan(`cd ${metadata.folderName}`)}`)
|
|
158
|
+
if (project.runHint) {
|
|
159
|
+
console.log(` ${chalk.dim(`# ${project.runHint}`)}`)
|
|
160
|
+
}
|
|
161
|
+
console.log()
|
|
162
|
+
console.log(chalk.dim(" Your work is being tracked. When you're done:"))
|
|
163
|
+
console.log()
|
|
164
|
+
console.log(` ${chalk.cyan("litmus submit")}`)
|
|
165
|
+
console.log()
|
|
166
|
+
if (metadata.deadline) {
|
|
167
|
+
const deadline = new Date(metadata.deadline)
|
|
168
|
+
console.log(chalk.dim(` Deadline: ${deadline.toLocaleString()}`))
|
|
169
|
+
console.log()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { statSync, unlinkSync } from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import chalk from "chalk"
|
|
4
|
+
import ora from "ora"
|
|
5
|
+
import * as readline from "readline/promises"
|
|
6
|
+
import { stdin as input, stdout as output } from "process"
|
|
7
|
+
import { readConfig } from "../lib/config.js"
|
|
8
|
+
import { submitZip } from "../lib/api.js"
|
|
9
|
+
import { createSubmissionZip } from "../lib/zip.js"
|
|
10
|
+
import { fatal, success, warn, info, FALLBACK_URL } from "../utils/errors.js"
|
|
11
|
+
|
|
12
|
+
export async function runSubmit(opts: { yes?: boolean }): Promise<void> {
|
|
13
|
+
// Step 1: Read config
|
|
14
|
+
const config = await readConfig()
|
|
15
|
+
|
|
16
|
+
if (!config) {
|
|
17
|
+
fatal(
|
|
18
|
+
"No Litmus assessment found in this directory.",
|
|
19
|
+
"Run this command from inside your assessment folder, or run 'litmus init <token>' first."
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Step 2: Check deadline
|
|
24
|
+
if (config.deadline) {
|
|
25
|
+
const deadline = new Date(config.deadline)
|
|
26
|
+
const now = new Date()
|
|
27
|
+
|
|
28
|
+
if (now > deadline) {
|
|
29
|
+
fatal(
|
|
30
|
+
`The submission deadline passed at ${deadline.toLocaleString()}.`,
|
|
31
|
+
`Contact the company if you need an extension. Visit ${FALLBACK_URL} for help.`
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const minsLeft = Math.floor((deadline.getTime() - now.getTime()) / (1000 * 60))
|
|
36
|
+
if (minsLeft < 30) {
|
|
37
|
+
warn(`Only ${minsLeft} minutes left before the deadline!`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Step 3: Find the project root (where .litmus/config.json lives)
|
|
42
|
+
let projectRoot = process.cwd()
|
|
43
|
+
// readConfig walks up the tree, so we use the config's assessed location
|
|
44
|
+
// The config file is at .litmus/config.json relative to the project root
|
|
45
|
+
// We find it by searching from cwd up, same as readConfig does
|
|
46
|
+
{
|
|
47
|
+
let dir = process.cwd()
|
|
48
|
+
while (true) {
|
|
49
|
+
const configPath = path.join(dir, ".litmus", "config.json")
|
|
50
|
+
const { existsSync } = await import("fs")
|
|
51
|
+
if (existsSync(configPath)) {
|
|
52
|
+
projectRoot = dir
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
const parent = path.dirname(dir)
|
|
56
|
+
if (parent === dir) break
|
|
57
|
+
dir = parent
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Step 4: Create the submission ZIP
|
|
62
|
+
const zipSpinner = ora("Preparing submission...").start()
|
|
63
|
+
let zipPath: string
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
zipPath = await createSubmissionZip(projectRoot)
|
|
67
|
+
zipSpinner.succeed("Submission prepared")
|
|
68
|
+
} catch (err) {
|
|
69
|
+
zipSpinner.fail("Failed to create submission archive")
|
|
70
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
71
|
+
fatal(msg, `Visit ${FALLBACK_URL} to submit manually.`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Step 5: Show what's being submitted
|
|
75
|
+
const zipSizeMb = (statSync(zipPath).size / (1024 * 1024)).toFixed(1)
|
|
76
|
+
const folderName = path.basename(projectRoot)
|
|
77
|
+
|
|
78
|
+
console.log()
|
|
79
|
+
console.log(chalk.bold(` Submitting: ${config.assessmentName}`))
|
|
80
|
+
info(` Archive size: ${zipSizeMb} MB`)
|
|
81
|
+
info(` Tracking log included`)
|
|
82
|
+
if (config.deadline) {
|
|
83
|
+
info(` Deadline: ${new Date(config.deadline).toLocaleString()}`)
|
|
84
|
+
}
|
|
85
|
+
console.log()
|
|
86
|
+
|
|
87
|
+
// Step 6: Confirm (unless --yes flag)
|
|
88
|
+
if (!opts.yes) {
|
|
89
|
+
const rl = readline.createInterface({ input, output })
|
|
90
|
+
let answer: string
|
|
91
|
+
try {
|
|
92
|
+
answer = await rl.question(
|
|
93
|
+
chalk.bold(" Confirm submission? ") + chalk.dim("[Y/n] ")
|
|
94
|
+
)
|
|
95
|
+
} finally {
|
|
96
|
+
rl.close()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const confirmed = answer.trim().toLowerCase()
|
|
100
|
+
if (confirmed !== "" && confirmed !== "y" && confirmed !== "yes") {
|
|
101
|
+
console.log()
|
|
102
|
+
console.log(chalk.dim(" Submission cancelled. Your work is saved."))
|
|
103
|
+
console.log()
|
|
104
|
+
try { unlinkSync(zipPath) } catch { /* cleanup */ }
|
|
105
|
+
process.exit(0)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log()
|
|
110
|
+
|
|
111
|
+
// Step 7: Upload
|
|
112
|
+
const uploadSpinner = ora("Uploading submission...").start()
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const fileName = `${folderName}_submission.zip`
|
|
116
|
+
const result = await submitZip(config.apiBase, config.token, zipPath, fileName)
|
|
117
|
+
uploadSpinner.succeed("Submission received")
|
|
118
|
+
|
|
119
|
+
console.log()
|
|
120
|
+
success("You're done!")
|
|
121
|
+
console.log()
|
|
122
|
+
console.log(
|
|
123
|
+
chalk.dim(" The company will review your work and be in touch.")
|
|
124
|
+
)
|
|
125
|
+
if (result.submissionId) {
|
|
126
|
+
console.log(chalk.dim(` Submission ID: ${result.submissionId}`))
|
|
127
|
+
}
|
|
128
|
+
console.log()
|
|
129
|
+
} catch (err) {
|
|
130
|
+
uploadSpinner.fail("Upload failed")
|
|
131
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
132
|
+
console.log()
|
|
133
|
+
fatal(
|
|
134
|
+
msg,
|
|
135
|
+
`To submit manually, visit ${FALLBACK_URL} and upload your ZIP from there.`
|
|
136
|
+
)
|
|
137
|
+
} finally {
|
|
138
|
+
// Clean up temp ZIP
|
|
139
|
+
try { unlinkSync(zipPath) } catch { /* cleanup */ }
|
|
140
|
+
}
|
|
141
|
+
}
|