jioogen 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +288 -71
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -363,9 +363,9 @@ var require_utils = __commonJS({
|
|
|
363
363
|
fpath.split(self.sep).forEach(function(name) {
|
|
364
364
|
if (!name || name.substr(-1, 1) === ":") return;
|
|
365
365
|
resolvedPath += self.sep + name;
|
|
366
|
-
var
|
|
366
|
+
var stat2;
|
|
367
367
|
try {
|
|
368
|
-
|
|
368
|
+
stat2 = self.fs.statSync(resolvedPath);
|
|
369
369
|
} catch (e) {
|
|
370
370
|
if (e.message && e.message.startsWith("ENOENT")) {
|
|
371
371
|
self.fs.mkdirSync(resolvedPath);
|
|
@@ -373,7 +373,7 @@ var require_utils = __commonJS({
|
|
|
373
373
|
throw e;
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
|
-
if (
|
|
376
|
+
if (stat2 && stat2.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
|
|
377
377
|
});
|
|
378
378
|
}
|
|
379
379
|
mkdirSync(folder);
|
|
@@ -382,8 +382,8 @@ var require_utils = __commonJS({
|
|
|
382
382
|
const self = this;
|
|
383
383
|
if (self.fs.existsSync(path)) {
|
|
384
384
|
if (!overwrite) return false;
|
|
385
|
-
var
|
|
386
|
-
if (
|
|
385
|
+
var stat2 = self.fs.statSync(path);
|
|
386
|
+
if (stat2.isDirectory()) {
|
|
387
387
|
return false;
|
|
388
388
|
}
|
|
389
389
|
}
|
|
@@ -416,8 +416,8 @@ var require_utils = __commonJS({
|
|
|
416
416
|
const self = this;
|
|
417
417
|
self.fs.exists(path, function(exist) {
|
|
418
418
|
if (exist && !overwrite) return callback(false);
|
|
419
|
-
self.fs.stat(path, function(err,
|
|
420
|
-
if (exist &&
|
|
419
|
+
self.fs.stat(path, function(err, stat2) {
|
|
420
|
+
if (exist && stat2 && stat2.isDirectory()) {
|
|
421
421
|
return callback(false);
|
|
422
422
|
}
|
|
423
423
|
var folder = pth.dirname(path);
|
|
@@ -469,13 +469,13 @@ var require_utils = __commonJS({
|
|
|
469
469
|
for (const part of rel.split(pth.sep)) {
|
|
470
470
|
if (!part || part === ".") continue;
|
|
471
471
|
cur = pth.join(cur, part);
|
|
472
|
-
let
|
|
472
|
+
let stat2;
|
|
473
473
|
try {
|
|
474
|
-
|
|
474
|
+
stat2 = self.fs.lstatSync(cur);
|
|
475
475
|
} catch (e) {
|
|
476
476
|
break;
|
|
477
477
|
}
|
|
478
|
-
if (
|
|
478
|
+
if (stat2.isSymbolicLink()) throw Errors.FILE_IN_THE_WAY(`"${cur}"`);
|
|
479
479
|
}
|
|
480
480
|
};
|
|
481
481
|
Utils.prototype.findFiles = function(path) {
|
|
@@ -502,11 +502,11 @@ var require_utils = __commonJS({
|
|
|
502
502
|
self.fs.readdirSync(dir).forEach(function(file) {
|
|
503
503
|
const path2 = pth.join(dir, file);
|
|
504
504
|
if (escapesRoot(path2)) return;
|
|
505
|
-
const
|
|
505
|
+
const stat2 = self.fs.statSync(path2);
|
|
506
506
|
if (!pattern || pattern.test(path2)) {
|
|
507
|
-
files.push(pth.normalize(path2) + (
|
|
507
|
+
files.push(pth.normalize(path2) + (stat2.isDirectory() ? self.sep : ""));
|
|
508
508
|
}
|
|
509
|
-
if (
|
|
509
|
+
if (stat2.isDirectory() && recursive) {
|
|
510
510
|
const realDir = self.fs.realpathSync(path2);
|
|
511
511
|
if (!visited.has(realDir)) {
|
|
512
512
|
visited.add(realDir);
|
|
@@ -553,14 +553,14 @@ var require_utils = __commonJS({
|
|
|
553
553
|
if (!--pending) done();
|
|
554
554
|
return;
|
|
555
555
|
}
|
|
556
|
-
self.fs.stat(file, function(err3,
|
|
556
|
+
self.fs.stat(file, function(err3, stat2) {
|
|
557
557
|
if (err3) return done(err3);
|
|
558
|
-
if (!
|
|
558
|
+
if (!stat2) {
|
|
559
559
|
if (!--pending) done();
|
|
560
560
|
return;
|
|
561
561
|
}
|
|
562
|
-
results.push(pth.normalize(file) + (
|
|
563
|
-
if (!
|
|
562
|
+
results.push(pth.normalize(file) + (stat2.isDirectory() ? self.sep : ""));
|
|
563
|
+
if (!stat2.isDirectory()) {
|
|
564
564
|
if (!--pending) done();
|
|
565
565
|
return;
|
|
566
566
|
}
|
|
@@ -7087,7 +7087,7 @@ var CANONICAL_CONCERNS = [
|
|
|
7087
7087
|
// ../contracts/src/execution.ts
|
|
7088
7088
|
var EXECUTION_SIDECAR_PATH = "tasks/.execution/state.json";
|
|
7089
7089
|
var EXECUTION_SIDECAR_VERSION = 1;
|
|
7090
|
-
var HANDOFF_PROMPT_VERSION =
|
|
7090
|
+
var HANDOFF_PROMPT_VERSION = 16;
|
|
7091
7091
|
var HANDOFF_AGENT_TOKEN_TTL_SECONDS = 30 * 24 * 3600;
|
|
7092
7092
|
var HandoffPayloadSchema = external_exports.object({
|
|
7093
7093
|
promptVersion: external_exports.literal(HANDOFF_PROMPT_VERSION),
|
|
@@ -8433,8 +8433,11 @@ function envValue(name) {
|
|
|
8433
8433
|
const value = process.env[name]?.trim();
|
|
8434
8434
|
return value !== void 0 && value.length > 0 ? value : void 0;
|
|
8435
8435
|
}
|
|
8436
|
+
function configDirOverride() {
|
|
8437
|
+
return envValue(CONFIG_DIR_ENV) ?? envValue(LEGACY_CONFIG_DIR_ENV);
|
|
8438
|
+
}
|
|
8436
8439
|
function configDir() {
|
|
8437
|
-
const override =
|
|
8440
|
+
const override = configDirOverride();
|
|
8438
8441
|
return override !== void 0 ? resolve(override) : join2(homedir(), ".config", "jioogen");
|
|
8439
8442
|
}
|
|
8440
8443
|
function legacyConfigDir() {
|
|
@@ -8474,6 +8477,7 @@ async function readCredentialsFile(path) {
|
|
|
8474
8477
|
async function readCredentials() {
|
|
8475
8478
|
const primary = await readCredentialsFile(credentialsPath());
|
|
8476
8479
|
if (primary !== null) return primary;
|
|
8480
|
+
if (configDirOverride() !== void 0) return null;
|
|
8477
8481
|
const legacyPath = join2(legacyConfigDir(), CREDENTIALS_FILE);
|
|
8478
8482
|
if (legacyPath === credentialsPath()) return null;
|
|
8479
8483
|
return readCredentialsFile(legacyPath);
|
|
@@ -8558,6 +8562,10 @@ function isMissingFile(error) {
|
|
|
8558
8562
|
return isRecord(error) && error.code === "ENOENT";
|
|
8559
8563
|
}
|
|
8560
8564
|
|
|
8565
|
+
// src/errors.ts
|
|
8566
|
+
var UsageError = class extends Error {
|
|
8567
|
+
};
|
|
8568
|
+
|
|
8561
8569
|
// src/output.ts
|
|
8562
8570
|
function formatTable(headers, rows) {
|
|
8563
8571
|
const widths = headers.map(
|
|
@@ -8569,11 +8577,52 @@ function formatTable(headers, rows) {
|
|
|
8569
8577
|
}
|
|
8570
8578
|
|
|
8571
8579
|
// src/sidecar.ts
|
|
8572
|
-
import {
|
|
8580
|
+
import { randomUUID } from "node:crypto";
|
|
8581
|
+
import { mkdir as mkdir2, open, readFile as readFile3, rename, rm as rm2, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
8573
8582
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
8583
|
+
var LOCK_RETRY_MS = 25;
|
|
8584
|
+
var LOCK_TIMEOUT_MS = 3e3;
|
|
8585
|
+
var LOCK_STALE_MS = 1e4;
|
|
8574
8586
|
function sidecarPath(bundleDir) {
|
|
8575
8587
|
return join3(bundleDir, EXECUTION_SIDECAR_PATH);
|
|
8576
8588
|
}
|
|
8589
|
+
function lockPath(bundleDir) {
|
|
8590
|
+
return `${sidecarPath(bundleDir)}.lock`;
|
|
8591
|
+
}
|
|
8592
|
+
async function withStateLock(bundleDir, fn) {
|
|
8593
|
+
const target = lockPath(bundleDir);
|
|
8594
|
+
await mkdir2(dirname2(target), { recursive: true });
|
|
8595
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
8596
|
+
let handle = null;
|
|
8597
|
+
let staleRemoved = false;
|
|
8598
|
+
while (handle === null) {
|
|
8599
|
+
try {
|
|
8600
|
+
handle = await open(target, "wx");
|
|
8601
|
+
} catch (error) {
|
|
8602
|
+
if (!isLockBusy(error)) throw error;
|
|
8603
|
+
if (!staleRemoved && await removeStaleLock(target)) {
|
|
8604
|
+
staleRemoved = true;
|
|
8605
|
+
continue;
|
|
8606
|
+
}
|
|
8607
|
+
if (Date.now() >= deadline) {
|
|
8608
|
+
throw new Error(
|
|
8609
|
+
`Gagal mengambil lock sidecar setelah ${LOCK_TIMEOUT_MS / 1e3} detik: ${target}. Kemungkinan proses lain masih menulis; hapus file lock itu bila proses tersebut sudah mati.`
|
|
8610
|
+
);
|
|
8611
|
+
}
|
|
8612
|
+
await delay(LOCK_RETRY_MS);
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
8615
|
+
try {
|
|
8616
|
+
await handle.writeFile(`${process.pid}
|
|
8617
|
+
`, "utf8");
|
|
8618
|
+
return await fn();
|
|
8619
|
+
} finally {
|
|
8620
|
+
await handle.close().catch(() => {
|
|
8621
|
+
});
|
|
8622
|
+
await rm2(target, { force: true }).catch(() => {
|
|
8623
|
+
});
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8577
8626
|
async function readState(bundleDir) {
|
|
8578
8627
|
let raw;
|
|
8579
8628
|
try {
|
|
@@ -8662,8 +8711,16 @@ async function writeState(bundleDir, state) {
|
|
|
8662
8711
|
const normalized = rebuild(state, state.tasks, state.current);
|
|
8663
8712
|
const target = sidecarPath(bundleDir);
|
|
8664
8713
|
await mkdir2(dirname2(target), { recursive: true });
|
|
8665
|
-
|
|
8714
|
+
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
8715
|
+
try {
|
|
8716
|
+
await writeFile2(temp, `${JSON.stringify(normalized, null, 2)}
|
|
8666
8717
|
`, "utf8");
|
|
8718
|
+
await rename(temp, target);
|
|
8719
|
+
} catch (error) {
|
|
8720
|
+
await rm2(temp, { force: true }).catch(() => {
|
|
8721
|
+
});
|
|
8722
|
+
throw error;
|
|
8723
|
+
}
|
|
8667
8724
|
return normalized;
|
|
8668
8725
|
}
|
|
8669
8726
|
function rebuild(state, tasks, current, now = /* @__PURE__ */ new Date()) {
|
|
@@ -8690,6 +8747,23 @@ function toTaskState(task) {
|
|
|
8690
8747
|
function isMissingFile2(error) {
|
|
8691
8748
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
8692
8749
|
}
|
|
8750
|
+
function isLockBusy(error) {
|
|
8751
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
8752
|
+
}
|
|
8753
|
+
async function removeStaleLock(target) {
|
|
8754
|
+
try {
|
|
8755
|
+
const info = await stat(target);
|
|
8756
|
+
if (Date.now() - info.mtimeMs < LOCK_STALE_MS) return false;
|
|
8757
|
+
await rm2(target, { force: true });
|
|
8758
|
+
return true;
|
|
8759
|
+
} catch (error) {
|
|
8760
|
+
if (isMissingFile2(error)) return true;
|
|
8761
|
+
throw error;
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
8764
|
+
function delay(ms) {
|
|
8765
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
8766
|
+
}
|
|
8693
8767
|
|
|
8694
8768
|
// src/skills.ts
|
|
8695
8769
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
@@ -9106,6 +9180,10 @@ async function extractBundle(bytes, targetDir) {
|
|
|
9106
9180
|
}
|
|
9107
9181
|
|
|
9108
9182
|
// src/commands.ts
|
|
9183
|
+
var PROJECT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9184
|
+
function isValidProjectId(projectId) {
|
|
9185
|
+
return PROJECT_ID_RE.test(projectId.trim());
|
|
9186
|
+
}
|
|
9109
9187
|
async function loginCommand(options) {
|
|
9110
9188
|
const token = options.token ?? envFirst("JIOOGEN_TOKEN", "JIOOCODE_TOKEN");
|
|
9111
9189
|
if (token !== void 0 && token.length > 0) {
|
|
@@ -9186,9 +9264,14 @@ async function projectsCommand(options) {
|
|
|
9186
9264
|
);
|
|
9187
9265
|
}
|
|
9188
9266
|
async function pullCommand(projectId, options) {
|
|
9267
|
+
if (!isValidProjectId(projectId)) {
|
|
9268
|
+
throw new UsageError(
|
|
9269
|
+
`Project id tidak valid: ${projectId}. Gunakan UUID dari \`jioogen projects\`.`
|
|
9270
|
+
);
|
|
9271
|
+
}
|
|
9189
9272
|
const { apiUrl, client } = await resolveApi(options);
|
|
9190
9273
|
const target = resolve4(options.dir ?? projectId);
|
|
9191
|
-
await assertPullTarget(target, projectId);
|
|
9274
|
+
await assertPullTarget(target, projectId, options.force === true);
|
|
9192
9275
|
const bytes = await client.getBytes(projectPath(projectId, "/bundle"));
|
|
9193
9276
|
const written = await extractBundle(bytes, target);
|
|
9194
9277
|
const skills = await installAgentSkills(target);
|
|
@@ -9203,6 +9286,28 @@ async function pullCommand(projectId, options) {
|
|
|
9203
9286
|
`Skills eksekusi terpasang: ${skills.written.length} file (AGENTS.md + 4 harness skill).`
|
|
9204
9287
|
);
|
|
9205
9288
|
}
|
|
9289
|
+
async function linkCommand(projectId, options) {
|
|
9290
|
+
const { apiUrl, client } = await resolveApi(options);
|
|
9291
|
+
const target = resolve4(options.dir ?? process.cwd());
|
|
9292
|
+
await assertLinkTarget(target, projectId);
|
|
9293
|
+
await verifyProjectAccess(client, projectId);
|
|
9294
|
+
await writeProjectLink(target, { projectId, apiUrl });
|
|
9295
|
+
const skills = await installAgentSkills(target);
|
|
9296
|
+
const linkedFile = join6(target, ".jioogen", "project.json");
|
|
9297
|
+
if (options.json === true) {
|
|
9298
|
+
console.log(
|
|
9299
|
+
JSON.stringify(
|
|
9300
|
+
{ projectId, target, apiUrl, linkedFile, skills: skills.written.length },
|
|
9301
|
+
null,
|
|
9302
|
+
2
|
|
9303
|
+
)
|
|
9304
|
+
);
|
|
9305
|
+
return;
|
|
9306
|
+
}
|
|
9307
|
+
console.log(`Project ${projectId} ter-bind ke ${target}.`);
|
|
9308
|
+
console.log(`File link: ${linkedFile} (API ${apiUrl}).`);
|
|
9309
|
+
console.log(`Skills eksekusi terpasang: ${skills.written.length} file.`);
|
|
9310
|
+
}
|
|
9206
9311
|
async function skillsInstallCommand(options) {
|
|
9207
9312
|
const link = await findProjectLink(options.dir ?? process.cwd());
|
|
9208
9313
|
const bundleDir = resolve4(link?.bundleDir ?? options.dir ?? process.cwd());
|
|
@@ -9242,9 +9347,15 @@ async function handoffCommand(options) {
|
|
|
9242
9347
|
}
|
|
9243
9348
|
}
|
|
9244
9349
|
async function taskNextCommand(options) {
|
|
9245
|
-
const {
|
|
9246
|
-
|
|
9247
|
-
|
|
9350
|
+
const { projectId, definitions, written, next } = await withLocalState(options, async (local) => {
|
|
9351
|
+
const written2 = await writeState(local.bundleDir, local.state);
|
|
9352
|
+
return {
|
|
9353
|
+
projectId: local.projectId,
|
|
9354
|
+
definitions: local.definitions,
|
|
9355
|
+
written: written2,
|
|
9356
|
+
next: selectNext(local.definitions, written2.tasks)
|
|
9357
|
+
};
|
|
9358
|
+
});
|
|
9248
9359
|
if (next === null) {
|
|
9249
9360
|
if (options.json === true) {
|
|
9250
9361
|
console.log(JSON.stringify({ done: true, projectId }, null, 2));
|
|
@@ -9293,62 +9404,71 @@ Status: ${next.state.status} \u2014 silakan ulangi.`);
|
|
|
9293
9404
|
Progress: ${written.counts?.done ?? 0}/${definitions.length} task selesai.`);
|
|
9294
9405
|
}
|
|
9295
9406
|
async function taskStartCommand(ref, options) {
|
|
9296
|
-
const
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9407
|
+
const task = await withLocalState(options, async (local) => {
|
|
9408
|
+
assertStartDependencies(ref, local.definitions, local.state.tasks);
|
|
9409
|
+
const updated = withStatus(local.state, ref, "in_progress", {
|
|
9410
|
+
awaitingCheckpoint: false,
|
|
9411
|
+
note: ""
|
|
9412
|
+
});
|
|
9413
|
+
await writeState(local.bundleDir, updated);
|
|
9414
|
+
return updated.tasks.find((entry) => entry.ref.toUpperCase() === ref.toUpperCase());
|
|
9300
9415
|
});
|
|
9301
|
-
await writeState(bundleDir, updated);
|
|
9302
|
-
const task = updated.tasks.find((entry) => entry.ref.toUpperCase() === ref.toUpperCase());
|
|
9303
9416
|
console.log(`Started ${task?.ref ?? ref} (attempt ${task?.attempt ?? 1}).`);
|
|
9304
9417
|
}
|
|
9305
9418
|
async function taskCompleteCommand(ref, options) {
|
|
9306
|
-
const {
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
const completedTask = completed.tasks.find(
|
|
9312
|
-
(entry) => entry.ref.toUpperCase() === ref.toUpperCase()
|
|
9313
|
-
);
|
|
9314
|
-
const next = selectNext(definitions, completed.tasks);
|
|
9315
|
-
if (completedTask !== void 0 && next !== null && isBoundary(completedTask, next.task)) {
|
|
9316
|
-
const withCheckpoint = setCurrent(completed, {
|
|
9317
|
-
ref: next.task.id,
|
|
9318
|
-
...next.task.layer !== void 0 ? { layer: next.task.layer } : {},
|
|
9319
|
-
phase: next.task.phase,
|
|
9320
|
-
awaitingCheckpoint: true
|
|
9419
|
+
const { completedTask, next, checkpoint } = await withLocalState(options, async (local) => {
|
|
9420
|
+
assertCompleteDependencies(ref, local.definitions, local.state.tasks, options.force === true);
|
|
9421
|
+
const completed = withStatus(local.state, ref, "done", {
|
|
9422
|
+
note: options.note ?? "",
|
|
9423
|
+
awaitingCheckpoint: false
|
|
9321
9424
|
});
|
|
9322
|
-
|
|
9323
|
-
|
|
9425
|
+
const completedTask2 = completed.tasks.find(
|
|
9426
|
+
(entry) => entry.ref.toUpperCase() === ref.toUpperCase()
|
|
9427
|
+
);
|
|
9428
|
+
const next2 = selectNext(local.definitions, completed.tasks);
|
|
9429
|
+
if (completedTask2 !== void 0 && next2 !== null && isBoundary(completedTask2, next2.task)) {
|
|
9430
|
+
const withCheckpoint = setCurrent(completed, {
|
|
9431
|
+
ref: next2.task.id,
|
|
9432
|
+
...next2.task.layer !== void 0 ? { layer: next2.task.layer } : {},
|
|
9433
|
+
phase: next2.task.phase,
|
|
9434
|
+
awaitingCheckpoint: true
|
|
9435
|
+
});
|
|
9436
|
+
await writeState(local.bundleDir, withCheckpoint);
|
|
9437
|
+
return { completedTask: completedTask2, next: next2, checkpoint: true };
|
|
9438
|
+
}
|
|
9439
|
+
await writeState(local.bundleDir, completed);
|
|
9440
|
+
return { completedTask: completedTask2, next: next2, checkpoint: false };
|
|
9441
|
+
});
|
|
9442
|
+
console.log(`Done ${completedTask?.ref ?? ref}.`);
|
|
9443
|
+
if (checkpoint && next !== null) {
|
|
9324
9444
|
console.log(
|
|
9325
9445
|
`Checkpoint berikutnya: ${next.task.id} (${next.task.layer ?? "-"} / phase ${next.task.phase}).`
|
|
9326
9446
|
);
|
|
9327
9447
|
console.log("Berhenti dan minta konfirmasi user sebelum task start berikutnya.");
|
|
9328
9448
|
return;
|
|
9329
9449
|
}
|
|
9330
|
-
await writeState(bundleDir, completed);
|
|
9331
|
-
console.log(`Done ${completedTask?.ref ?? ref}.`);
|
|
9332
9450
|
if (next !== null) console.log(`Berikutnya: ${next.task.id} \u2014 ${next.task.title}`);
|
|
9333
9451
|
}
|
|
9334
9452
|
async function taskFailCommand(ref, reasonParts, options) {
|
|
9335
9453
|
const reason = reasonParts.join(" ").trim() || options.note || "";
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9454
|
+
await withLocalState(options, async (local) => {
|
|
9455
|
+
const updated = withStatus(local.state, ref, "failed", {
|
|
9456
|
+
note: reason,
|
|
9457
|
+
awaitingCheckpoint: false
|
|
9458
|
+
});
|
|
9459
|
+
await writeState(local.bundleDir, updated);
|
|
9340
9460
|
});
|
|
9341
|
-
await writeState(bundleDir, updated);
|
|
9342
9461
|
console.log(`Failed ${ref}${reason.length > 0 ? `: ${reason}` : "."}`);
|
|
9343
9462
|
}
|
|
9344
9463
|
async function taskResetCommand(ref, reasonParts, options) {
|
|
9345
9464
|
const reason = reasonParts.join(" ").trim() || options.note || "";
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9465
|
+
await withLocalState(options, async (local) => {
|
|
9466
|
+
const updated = withStatus(local.state, ref, "todo", {
|
|
9467
|
+
note: reason,
|
|
9468
|
+
awaitingCheckpoint: false
|
|
9469
|
+
});
|
|
9470
|
+
await writeState(local.bundleDir, updated);
|
|
9350
9471
|
});
|
|
9351
|
-
await writeState(bundleDir, updated);
|
|
9352
9472
|
console.log(`Reset ${ref} ke todo${reason.length > 0 ? `: ${reason}` : "."}`);
|
|
9353
9473
|
}
|
|
9354
9474
|
async function statusCommand(options) {
|
|
@@ -9359,6 +9479,23 @@ async function statusCommand(options) {
|
|
|
9359
9479
|
const { bundleDir, projectId } = await resolveLocalBundle(options);
|
|
9360
9480
|
const existing = await readState(bundleDir);
|
|
9361
9481
|
if (existing === null) {
|
|
9482
|
+
if (options.json === true) {
|
|
9483
|
+
console.log(
|
|
9484
|
+
JSON.stringify(
|
|
9485
|
+
{
|
|
9486
|
+
projectId,
|
|
9487
|
+
hasExecution: false,
|
|
9488
|
+
tasks: [],
|
|
9489
|
+
counts: summarizeExecutionCounts([]),
|
|
9490
|
+
current: null,
|
|
9491
|
+
updatedAt: null
|
|
9492
|
+
},
|
|
9493
|
+
null,
|
|
9494
|
+
2
|
|
9495
|
+
)
|
|
9496
|
+
);
|
|
9497
|
+
return;
|
|
9498
|
+
}
|
|
9362
9499
|
console.log(`Project ${projectId}: belum ada sidecar. Jalankan \`jioogen task next\`.`);
|
|
9363
9500
|
return;
|
|
9364
9501
|
}
|
|
@@ -9465,14 +9602,40 @@ async function resolveBundle(options) {
|
|
|
9465
9602
|
const api = await resolveApi({ api: options.api ?? local.linkedApiUrl });
|
|
9466
9603
|
return { ...api, ...local };
|
|
9467
9604
|
}
|
|
9468
|
-
async function
|
|
9605
|
+
async function withLocalState(options, fn) {
|
|
9469
9606
|
const context = await resolveLocalBundle(options);
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9607
|
+
return withStateLock(context.bundleDir, async () => {
|
|
9608
|
+
const definitions = await loadTasks(context.bundleDir);
|
|
9609
|
+
const existing = await readState(context.bundleDir);
|
|
9610
|
+
const state = existing === null ? createState(context.projectId, definitions) : mergeTasks(existing, definitions);
|
|
9611
|
+
return fn({ ...context, definitions, state });
|
|
9612
|
+
});
|
|
9474
9613
|
}
|
|
9475
|
-
|
|
9614
|
+
function unmetDependencies(ref, definitions, states) {
|
|
9615
|
+
const key = ref.toUpperCase();
|
|
9616
|
+
const definition = definitions.find((task) => task.id.toUpperCase() === key);
|
|
9617
|
+
if (definition === void 0) return [];
|
|
9618
|
+
const byRef = new Map(states.map((state) => [state.ref.toUpperCase(), state]));
|
|
9619
|
+
return definition.dependsOn.filter(
|
|
9620
|
+
(dependency) => byRef.get(dependency.toUpperCase())?.status !== "done"
|
|
9621
|
+
);
|
|
9622
|
+
}
|
|
9623
|
+
function assertStartDependencies(ref, definitions, states) {
|
|
9624
|
+
const missing = unmetDependencies(ref, definitions, states);
|
|
9625
|
+
if (missing.length === 0) return;
|
|
9626
|
+
throw new Error(
|
|
9627
|
+
`Tidak bisa memulai ${ref}: dependency belum selesai (${missing.join(", ")}). Selesaikan dependency itu lebih dulu.`
|
|
9628
|
+
);
|
|
9629
|
+
}
|
|
9630
|
+
function assertCompleteDependencies(ref, definitions, states, force) {
|
|
9631
|
+
if (force) return;
|
|
9632
|
+
const missing = unmetDependencies(ref, definitions, states);
|
|
9633
|
+
if (missing.length === 0) return;
|
|
9634
|
+
throw new Error(
|
|
9635
|
+
`Tidak bisa menyelesaikan ${ref}: dependency belum selesai (${missing.join(", ")}). Selesaikan dependency itu lebih dulu, atau tambahkan --force bila memang disengaja.`
|
|
9636
|
+
);
|
|
9637
|
+
}
|
|
9638
|
+
async function assertPullTarget(target, projectId, force = false) {
|
|
9476
9639
|
let entries;
|
|
9477
9640
|
try {
|
|
9478
9641
|
entries = await readdir3(target);
|
|
@@ -9488,6 +9651,53 @@ async function assertPullTarget(target, projectId) {
|
|
|
9488
9651
|
if (link.projectId !== projectId) {
|
|
9489
9652
|
throw new Error(`Direktori ${target} terikat ke project ${link.projectId}.`);
|
|
9490
9653
|
}
|
|
9654
|
+
if (!force) {
|
|
9655
|
+
throw new Error(
|
|
9656
|
+
`Direktori ${target} sudah berisi bundle project ini; pull ulang akan menimpa edit lokal. Jalankan \`jioogen link ${projectId}\` bila hanya ingin mengikat, atau ulangi dengan --force.`
|
|
9657
|
+
);
|
|
9658
|
+
}
|
|
9659
|
+
}
|
|
9660
|
+
async function assertLinkTarget(target, projectId) {
|
|
9661
|
+
if (!await hasBundleMarkers(target)) {
|
|
9662
|
+
throw new Error(
|
|
9663
|
+
`Direktori ${target} tidak berisi bundle JiooGen (butuh PRD.md atau tasks/*.md). Jalankan \`jioogen pull ${projectId} --dir <dir-kosong>\` untuk mengunduh bundle baru.`
|
|
9664
|
+
);
|
|
9665
|
+
}
|
|
9666
|
+
const link = await findProjectLink(target);
|
|
9667
|
+
if (link !== null && link.projectId !== projectId) {
|
|
9668
|
+
throw new Error(
|
|
9669
|
+
`Direktori ${target} sudah terikat ke project ${link.projectId}. Hapus .jioogen/project.json di sana atau pilih --dir lain.`
|
|
9670
|
+
);
|
|
9671
|
+
}
|
|
9672
|
+
}
|
|
9673
|
+
async function hasBundleMarkers(target) {
|
|
9674
|
+
try {
|
|
9675
|
+
await readFile6(join6(target, "PRD.md"));
|
|
9676
|
+
return true;
|
|
9677
|
+
} catch (error) {
|
|
9678
|
+
if (!isMissingFile4(error)) throw error;
|
|
9679
|
+
}
|
|
9680
|
+
return hasTaskMarkdown(join6(target, "tasks"));
|
|
9681
|
+
}
|
|
9682
|
+
async function hasTaskMarkdown(directory) {
|
|
9683
|
+
let names;
|
|
9684
|
+
try {
|
|
9685
|
+
names = await readdir3(directory);
|
|
9686
|
+
} catch (error) {
|
|
9687
|
+
if (isMissingFile4(error)) return false;
|
|
9688
|
+
throw error;
|
|
9689
|
+
}
|
|
9690
|
+
return names.some((name) => name.toLowerCase().endsWith(".md"));
|
|
9691
|
+
}
|
|
9692
|
+
async function verifyProjectAccess(client, projectId) {
|
|
9693
|
+
try {
|
|
9694
|
+
await client.getJson(projectPath(projectId, "/execution"));
|
|
9695
|
+
} catch (error) {
|
|
9696
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
9697
|
+
throw new Error(`Project ${projectId} tidak ditemukan atau bukan milik akun ini.`);
|
|
9698
|
+
}
|
|
9699
|
+
throw error;
|
|
9700
|
+
}
|
|
9491
9701
|
}
|
|
9492
9702
|
function assertBundleRelative(path) {
|
|
9493
9703
|
const normalized = path.replace(/\\/g, "/");
|
|
@@ -9541,7 +9751,7 @@ function isMissingFile4(error) {
|
|
|
9541
9751
|
}
|
|
9542
9752
|
|
|
9543
9753
|
// src/index.ts
|
|
9544
|
-
var VERSION = "0.2.
|
|
9754
|
+
var VERSION = "0.2.2";
|
|
9545
9755
|
var USAGE = `jioogen \u2014 CLI JiooGen (task selection lokal, server read-only)
|
|
9546
9756
|
|
|
9547
9757
|
Usage:
|
|
@@ -9550,13 +9760,14 @@ Usage:
|
|
|
9550
9760
|
jioogen logout
|
|
9551
9761
|
jioogen whoami
|
|
9552
9762
|
jioogen projects
|
|
9553
|
-
jioogen pull <projectId> [--dir <dir>]
|
|
9763
|
+
jioogen pull <projectId> [--dir <dir>] [--force]
|
|
9764
|
+
jioogen link <projectId> [--dir <dir>]
|
|
9554
9765
|
jioogen skills install [--dir <dir>]
|
|
9555
9766
|
jioogen check [--dir <dir>] [--json]
|
|
9556
9767
|
jioogen handoff [--project <id>] [--json]
|
|
9557
9768
|
jioogen task next [--json]
|
|
9558
9769
|
jioogen task start <ref>
|
|
9559
|
-
jioogen task complete <ref> [--note <text>]
|
|
9770
|
+
jioogen task complete <ref> [--note <text>] [--force]
|
|
9560
9771
|
jioogen task fail <ref> [reason...] [--note <text>]
|
|
9561
9772
|
jioogen task reset <ref> [reason...] [--note <text>]
|
|
9562
9773
|
jioogen status [--remote] [--json]
|
|
@@ -9572,8 +9783,6 @@ Global options:
|
|
|
9572
9783
|
-h, --help Tampilkan bantuan
|
|
9573
9784
|
-v, --version Tampilkan versi
|
|
9574
9785
|
`;
|
|
9575
|
-
var UsageError = class extends Error {
|
|
9576
|
-
};
|
|
9577
9786
|
async function main() {
|
|
9578
9787
|
let raw = {};
|
|
9579
9788
|
let positionals = [];
|
|
@@ -9590,6 +9799,7 @@ async function main() {
|
|
|
9590
9799
|
project: { type: "string" },
|
|
9591
9800
|
dir: { type: "string" },
|
|
9592
9801
|
remote: { type: "boolean" },
|
|
9802
|
+
force: { type: "boolean" },
|
|
9593
9803
|
files: { type: "string" },
|
|
9594
9804
|
note: { type: "string" },
|
|
9595
9805
|
json: { type: "boolean" },
|
|
@@ -9615,6 +9825,7 @@ async function main() {
|
|
|
9615
9825
|
project: text(raw.project),
|
|
9616
9826
|
dir: text(raw.dir),
|
|
9617
9827
|
remote: flag(raw.remote),
|
|
9828
|
+
force: flag(raw.force),
|
|
9618
9829
|
files: text(raw.files),
|
|
9619
9830
|
note: text(raw.note),
|
|
9620
9831
|
json: flag(raw.json)
|
|
@@ -9667,6 +9878,12 @@ async function dispatch(command, rest, options) {
|
|
|
9667
9878
|
await pullCommand(projectId, options);
|
|
9668
9879
|
return;
|
|
9669
9880
|
}
|
|
9881
|
+
case "link": {
|
|
9882
|
+
const projectId = rest[0];
|
|
9883
|
+
if (projectId === void 0) throw new UsageError("`jioogen link` butuh <projectId>.");
|
|
9884
|
+
await linkCommand(projectId, options);
|
|
9885
|
+
return;
|
|
9886
|
+
}
|
|
9670
9887
|
case "skills":
|
|
9671
9888
|
await dispatchSkills(rest, options);
|
|
9672
9889
|
return;
|