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 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 stat;
366
+ var stat2;
367
367
  try {
368
- stat = self.fs.statSync(resolvedPath);
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 (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
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 stat = self.fs.statSync(path);
386
- if (stat.isDirectory()) {
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, stat) {
420
- if (exist && stat && stat.isDirectory()) {
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 stat;
472
+ let stat2;
473
473
  try {
474
- stat = self.fs.lstatSync(cur);
474
+ stat2 = self.fs.lstatSync(cur);
475
475
  } catch (e) {
476
476
  break;
477
477
  }
478
- if (stat.isSymbolicLink()) throw Errors.FILE_IN_THE_WAY(`"${cur}"`);
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 stat = self.fs.statSync(path2);
505
+ const stat2 = self.fs.statSync(path2);
506
506
  if (!pattern || pattern.test(path2)) {
507
- files.push(pth.normalize(path2) + (stat.isDirectory() ? self.sep : ""));
507
+ files.push(pth.normalize(path2) + (stat2.isDirectory() ? self.sep : ""));
508
508
  }
509
- if (stat.isDirectory() && recursive) {
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, stat) {
556
+ self.fs.stat(file, function(err3, stat2) {
557
557
  if (err3) return done(err3);
558
- if (!stat) {
558
+ if (!stat2) {
559
559
  if (!--pending) done();
560
560
  return;
561
561
  }
562
- results.push(pth.normalize(file) + (stat.isDirectory() ? self.sep : ""));
563
- if (!stat.isDirectory()) {
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 = 15;
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 = envValue(CONFIG_DIR_ENV) ?? envValue(LEGACY_CONFIG_DIR_ENV);
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 { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
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
- await writeFile2(target, `${JSON.stringify(normalized, null, 2)}
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 { bundleDir, state, definitions, projectId } = await loadLocalState(options);
9246
- const written = await writeState(bundleDir, state);
9247
- const next = selectNext(definitions, written.tasks);
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 { bundleDir, state } = await loadLocalState(options);
9297
- const updated = withStatus(state, ref, "in_progress", {
9298
- awaitingCheckpoint: false,
9299
- note: ""
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 { bundleDir, state, definitions } = await loadLocalState(options);
9307
- const completed = withStatus(state, ref, "done", {
9308
- note: options.note ?? "",
9309
- awaitingCheckpoint: false
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
- await writeState(bundleDir, withCheckpoint);
9323
- console.log(`Done ${completedTask.ref}.`);
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
- const { bundleDir, state } = await loadLocalState(options);
9337
- const updated = withStatus(state, ref, "failed", {
9338
- note: reason,
9339
- awaitingCheckpoint: false
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
- const { bundleDir, state } = await loadLocalState(options);
9347
- const updated = withStatus(state, ref, "todo", {
9348
- note: reason,
9349
- awaitingCheckpoint: false
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 loadLocalState(options) {
9605
+ async function withLocalState(options, fn) {
9469
9606
  const context = await resolveLocalBundle(options);
9470
- const definitions = await loadTasks(context.bundleDir);
9471
- const existing = await readState(context.bundleDir);
9472
- const state = existing === null ? createState(context.projectId, definitions) : mergeTasks(existing, definitions);
9473
- return { ...context, definitions, state };
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
- async function assertPullTarget(target, projectId) {
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.0";
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;